diff --git a/samples/TestApplicationShared/GalleryStyle.cs b/samples/TestApplicationShared/GalleryStyle.cs index a0e32b7740..b29e22d2ec 100644 --- a/samples/TestApplicationShared/GalleryStyle.cs +++ b/samples/TestApplicationShared/GalleryStyle.cs @@ -19,7 +19,7 @@ namespace TestApplication { this.AddRange(new[] { - new Style (s => s.Class(":container").OfType ()) + new Style (s => s.Class("container").OfType ()) { Setters = new[] { @@ -27,15 +27,15 @@ namespace TestApplication } }, - new Style(s => s.Class(":container").OfType().Child().Child().Child().Child().Child().OfType()) + new Style(s => s.Class("container").OfType().Child().Child().Child().Child().Child().OfType()) { Setters = new[] { - new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate (TabItemTemplate)), + new Setter (TemplatedControl.TemplateProperty, new FuncControlTemplate(TabStripItemTemplate)), } }, - new Style(s => s.Name("internalStrip").OfType().Child().OfType()) + new Style(s => s.Name("PART_TabStrip").OfType().Child().OfType()) { Setters = new[] { @@ -44,7 +44,7 @@ namespace TestApplication } }, - new Style(s => s.Name("internalStrip").OfType().Child().OfType().Class(":selected")) + new Style(s => s.Name("PART_TabStrip").OfType().Child().OfType().Class(":selected")) { Setters = new[] { @@ -55,7 +55,7 @@ namespace TestApplication }); } - public static Control TabItemTemplate(TabItem control) + public static Control TabStripItemTemplate(TabStripItem control) { return new ContentPresenter { @@ -72,8 +72,8 @@ namespace TestApplication } }) }, - Name = "headerPresenter", - [~ContentPresenter.ContentProperty] = control[~HeaderedContentControl.HeaderProperty], + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; } @@ -96,9 +96,10 @@ namespace TestApplication { Content = new TabStrip { + Name = "PART_TabStrip", ItemsPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical, Gap = 4 }), Margin = new Thickness(0, 10, 0, 0), - Name = "internalStrip", + MemberSelector = TabControl.HeaderSelector, [!ItemsControl.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!!SelectingItemsControl.SelectedItemProperty] = control[!!SelectingItemsControl.SelectedItemProperty], } @@ -106,8 +107,8 @@ namespace TestApplication }, new Carousel { - Name = "carousel", - MemberSelector = control.ContentSelector, + Name = "PART_Content", + MemberSelector = TabControl.ContentSelector, [~Carousel.TransitionProperty] = control[~TabControl.TransitionProperty], [!Carousel.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!Carousel.SelectedItemProperty] = control[!SelectingItemsControl.SelectedItemProperty], diff --git a/samples/TestApplicationShared/MainWindow.cs b/samples/TestApplicationShared/MainWindow.cs index 2a0c736984..e702b6f4a5 100644 --- a/samples/TestApplicationShared/MainWindow.cs +++ b/samples/TestApplicationShared/MainWindow.cs @@ -104,8 +104,7 @@ namespace TestApplication }; - container.Classes.Add(":container"); - + container.Classes.Add("container"); window.Show(); return window; diff --git a/samples/XamlTestApplicationPcl/TestScrollable.cs b/samples/XamlTestApplicationPcl/TestScrollable.cs new file mode 100644 index 0000000000..cc5552375b --- /dev/null +++ b/samples/XamlTestApplicationPcl/TestScrollable.cs @@ -0,0 +1,84 @@ +using System; +using Perspex; +using Perspex.Controls; +using Perspex.Controls.Primitives; +using Perspex.Media; + +namespace XamlTestApplication +{ + public class TestScrollable : Control, IScrollable + { + private int itemCount = 100; + private Size _extent; + private Vector _offset; + private Size _viewport; + private Size _lineSize; + + public Action InvalidateScroll { get; set; } + + Size IScrollable.Extent + { + get { return _extent; } + } + + Vector IScrollable.Offset + { + get { return _offset; } + + set + { + _offset = value; + InvalidateVisual(); + } + } + + Size IScrollable.Viewport + { + get { return _viewport; } + } + + protected override Size MeasureOverride(Size availableSize) + { + using (var line = new FormattedText( + "Item 100", + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + line.Constraint = availableSize; + _lineSize = line.Measure(); + return new Size(_lineSize.Width, _lineSize.Height * itemCount); + } + } + + protected override Size ArrangeOverride(Size finalSize) + { + _viewport = new Size(finalSize.Width, finalSize.Height / _lineSize.Height); + _extent = new Size(_lineSize.Width, itemCount + 1); + InvalidateScroll?.Invoke(); + return finalSize; + } + + public override void Render(DrawingContext context) + { + var y = 0.0; + + for (var i = (int)_offset.Y; i < itemCount; ++i) + { + using (var line = new FormattedText( + "Item " + (i + 1), + TextBlock.GetFontFamily(this), + TextBlock.GetFontSize(this), + TextBlock.GetFontStyle(this), + TextAlignment.Left, + TextBlock.GetFontWeight(this))) + { + context.DrawText(Brushes.Black, new Point(-_offset.X, y), line); + y += _lineSize.Height; + } + } + } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index afc2d592e9..945b9cb1b0 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -1,15 +1,16 @@  - + - + @@ -285,6 +286,11 @@ + + + + + \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj index 46358e5836..a32ae5eec6 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj +++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs index 1aa1660079..81cf3b0cdc 100644 --- a/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs @@ -5,6 +5,7 @@ using System; using System.Globalization; using OmniXaml.TypeConversion; using Perspex.Media.Imaging; +using Perspex.Platform; namespace Perspex.Markup.Xaml.Converters { @@ -22,7 +23,19 @@ namespace Perspex.Markup.Xaml.Converters public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) { - return new Bitmap((string)value); + var uri = new Uri((string)value, UriKind.RelativeOrAbsolute); + var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file"; + + switch (scheme) + { + case "file": + return new Bitmap((string)value); + case "resource": + var assets = PerspexLocator.Current.GetService(); + return new Bitmap(assets.Open(uri)); + default: + throw new NotSupportedException($"Unsupported bitmap URI scheme: {uri.Scheme}."); + } } public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs index 3f49bc04fe..e2e7c4f501 100644 --- a/src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs +++ b/src/Markup/Perspex.Markup.Xaml/Converters/ClassesTypeConverter.cs @@ -4,6 +4,7 @@ using System; using System.Globalization; using OmniXaml.TypeConversion; +using Perspex.Controls; using Perspex.Styling; namespace Perspex.Markup.Xaml.Converters diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index b220fb4e46..00ed242855 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.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 System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Controls; @@ -216,15 +217,13 @@ namespace Perspex.Markup.Xaml.Data if (!targetIsDataContext) { + var update = target.GetObservable(Control.DataContextProperty) + .Skip(1) + .Select(_ => Unit.Default); var result = new ExpressionObserver( () => target.GetValue(Control.DataContextProperty), - path); - - /// TODO: Instead of doing this, make the ExpressionObserver accept an "update" - /// observable as doing it this way can will cause a leak in Binding as this - /// observable is never unsubscribed. - target.GetObservable(Control.DataContextProperty).Subscribe(x => - result.UpdateRoot()); + path, + update); return result; } @@ -245,19 +244,14 @@ namespace Perspex.Markup.Xaml.Data { Contract.Requires(target != null); + var update = target.GetObservable(Control.TemplatedParentProperty) + .Skip(1) + .Select(_ => Unit.Default); + var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), - path); - - if (target.GetValue(Control.TemplatedParentProperty) == null) - { - // TemplatedParent should only be set once, so only listen for the first non-null - // value. - target.GetObservable(Control.TemplatedParentProperty) - .Where(x => x != null) - .Take(1) - .Subscribe(x => result.UpdateRoot()); - } + path, + update); return result; } diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index e8df013d50..52e619872f 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Markup.Data.Plugins; @@ -29,10 +30,11 @@ namespace Perspex.Markup.Data private readonly object _root; private readonly Func _rootGetter; private readonly IObservable _rootObservable; + private readonly IObservable _update; private IDisposable _rootObserverSubscription; + private IDisposable _updateSubscription; private int _count; private readonly ExpressionNode _node; - private ISubject _empty; /// /// Initializes a new instance of the class. @@ -78,12 +80,18 @@ namespace Perspex.Markup.Data /// /// A function which gets the root object. /// The expression. - public ExpressionObserver(Func rootGetter, string expression) + /// An observable which triggers a re-read of the getter. + public ExpressionObserver( + Func rootGetter, + string expression, + IObservable update) { Contract.Requires(rootGetter != null); Contract.Requires(expression != null); + Contract.Requires(update != null); _rootGetter = rootGetter; + _update = update; if (!string.IsNullOrWhiteSpace(expression)) { @@ -104,7 +112,11 @@ namespace Perspex.Markup.Data public bool SetValue(object value) { IncrementCount(); - UpdateRoot(); + + if (_rootGetter != null && _node != null) + { + _node.Target = _rootGetter(); + } try { @@ -156,6 +168,11 @@ namespace Perspex.Markup.Data /// string IDescription.Description => Expression; + /// + /// Gets the root expression node. Used for testing. + /// + internal ExpressionNode Node => _node; + /// /// Gets the leaf node. /// @@ -169,26 +186,6 @@ namespace Perspex.Markup.Data } } - /// - /// Causes the root object to be re-read from the root getter. - /// - /// TODO: Instead of doing this, make the object accept an "update" observable - /// as doing it this way can cause a leak in Binding. - public void UpdateRoot() - { - if (_count > 0 && _rootGetter != null) - { - if (_node != null) - { - _node.Target = _rootGetter(); - } - else - { - _empty?.OnNext(_rootGetter()); - } - } - } - /// protected override IDisposable SubscribeCore(IObserver observer) { @@ -204,14 +201,23 @@ namespace Perspex.Markup.Data subscription.Dispose(); }); } + else if (_rootObservable != null) + { + return _rootObservable.Subscribe(observer); + } else { - if (_empty == null) + if (_update == null) { - _empty = new BehaviorSubject(_rootGetter()); + return Observable.Never().StartWith(_root).Subscribe(observer); + } + else + { + return _update + .Select(_ => _rootGetter()) + .StartWith(_rootGetter()) + .Subscribe(observer); } - - return _empty.Subscribe(observer); } } @@ -222,6 +228,11 @@ namespace Perspex.Markup.Data if (_rootGetter != null) { _node.Target = _rootGetter(); + + if (_update != null) + { + _updateSubscription = _update.Subscribe(x => _node.Target = _rootGetter()); + } } else if (_rootObservable != null) { @@ -244,6 +255,12 @@ namespace Perspex.Markup.Data _rootObserverSubscription = null; } + if (_updateSubscription != null) + { + _updateSubscription.Dispose(); + _updateSubscription = null; + } + _node.Target = null; } } diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 52d2574c70..177c337d04 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -32,7 +32,7 @@ namespace Perspex /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles + public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot { static Action _platformInitializationCallback; @@ -135,6 +135,11 @@ namespace Perspex protected set; } + /// + /// Gets the styling parent of the application, which is null. + /// + IStyleHost IStyleHost.StylingParent => null; + /// /// Runs the application's main loop until the is closed. /// diff --git a/src/Perspex.Base/BindingDescriptor.cs b/src/Perspex.Base/BindingDescriptor.cs index 4ee4966a98..c0c8f3406f 100644 --- a/src/Perspex.Base/BindingDescriptor.cs +++ b/src/Perspex.Base/BindingDescriptor.cs @@ -78,6 +78,18 @@ namespace Perspex set; } + /// + /// Gets or sets the source observable. + /// + /// + /// If null, then . will be used. + /// + public IObservable SourceObservable + { + get; + set; + } + /// /// Gets a description of the binding. /// @@ -128,7 +140,7 @@ namespace Perspex /// protected override IDisposable SubscribeCore(IObserver observer) { - return Source.GetObservable(Property).Subscribe(observer); + return (SourceObservable ?? Source.GetObservable(Property)).Subscribe(observer); } } } diff --git a/src/Perspex.Base/Collections/PerspexList.cs b/src/Perspex.Base/Collections/PerspexList.cs index 25424461db..3cb3b34ad0 100644 --- a/src/Perspex.Base/Collections/PerspexList.cs +++ b/src/Perspex.Base/Collections/PerspexList.cs @@ -52,7 +52,7 @@ namespace Perspex.Collections /// /// /// - public class PerspexList : IPerspexList, IList, INotifyCollectionChanged, INotifyPropertyChanged + public class PerspexList : IPerspexList, IList { private List _inner; @@ -150,7 +150,8 @@ namespace Perspex.Collections var e = new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, value, - old); + old, + index); CollectionChanged(this, e); } } @@ -171,7 +172,7 @@ namespace Perspex.Collections /// Adds an item to the collection. /// /// The item. - public void Add(T item) + public virtual void Add(T item) { Validate?.Invoke(item); int index = _inner.Count; @@ -183,7 +184,7 @@ namespace Perspex.Collections /// Adds multiple items to the collection. /// /// The items. - public void AddRange(IEnumerable items) + public virtual void AddRange(IEnumerable items) { Contract.Requires(items != null); @@ -247,6 +248,16 @@ namespace Perspex.Collections return _inner.GetEnumerator(); } + /// + /// Gets a range of items from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + public IEnumerable GetRange(int index, int count) + { + return _inner.GetRange(index, count); + } + /// /// Gets the index of the specified item in the collection. /// @@ -264,7 +275,7 @@ namespace Perspex.Collections /// /// The index. /// The item. - public void Insert(int index, T item) + public virtual void Insert(int index, T item) { Validate?.Invoke(item); _inner.Insert(index, item); @@ -276,7 +287,7 @@ namespace Perspex.Collections /// /// The index. /// The items. - public void InsertRange(int index, IEnumerable items) + public virtual void InsertRange(int index, IEnumerable items) { Contract.Requires(items != null); @@ -302,7 +313,7 @@ namespace Perspex.Collections /// /// The item. /// True if the item was found and removed, otherwise false. - public bool Remove(T item) + public virtual bool Remove(T item) { int index = _inner.IndexOf(item); @@ -320,7 +331,7 @@ namespace Perspex.Collections /// Removes multiple items from the collection. /// /// The items. - public void RemoveAll(IEnumerable items) + public virtual void RemoveAll(IEnumerable items) { Contract.Requires(items != null); @@ -337,7 +348,7 @@ namespace Perspex.Collections /// Removes the item at the specified index. /// /// The index. - public void RemoveAt(int index) + public virtual void RemoveAt(int index) { T item = _inner[index]; _inner.RemoveAt(index); @@ -349,7 +360,7 @@ namespace Perspex.Collections /// /// The first index to remove. /// The number of items to remove. - public void RemoveRange(int index, int count) + public virtual void RemoveRange(int index, int count) { if (count > 0) { diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 70dff41dda..25e0ff48be 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -163,13 +163,7 @@ namespace Perspex { get { - return new BindingDescriptor - { - Mode = binding.Mode, - Priority = binding.Priority, - Property = binding.Property, - Source = this, - }; + return CreateBindingDescriptor(binding); } set @@ -203,6 +197,17 @@ namespace Perspex } } + protected virtual BindingDescriptor CreateBindingDescriptor(BindingDescriptor source) + { + return new BindingDescriptor + { + Mode = source.Mode, + Priority = source.Priority, + Property = source.Property, + Source = this, + }; + } + public bool CheckAccess() => Dispatcher.UIThread.CheckAccess(); public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess(); diff --git a/src/Perspex.Controls/Button.cs b/src/Perspex.Controls/Button.cs index c334007b70..71dbbf22e0 100644 --- a/src/Perspex.Controls/Button.cs +++ b/src/Perspex.Controls/Button.cs @@ -215,7 +215,7 @@ namespace Perspex.Controls { base.OnPointerPressed(e); - Classes.Add(":pressed"); + PseudoClasses.Add(":pressed"); e.Device.Capture(this); e.Handled = true; @@ -231,7 +231,7 @@ namespace Perspex.Controls base.OnPointerReleased(e); e.Device.Capture(null); - Classes.Remove(":pressed"); + PseudoClasses.Remove(":pressed"); e.Handled = true; if (ClickMode == ClickMode.Release && Classes.Contains(":pointerover")) diff --git a/src/Perspex.Controls/Classes.cs b/src/Perspex.Controls/Classes.cs new file mode 100644 index 0000000000..61f4e8368d --- /dev/null +++ b/src/Perspex.Controls/Classes.cs @@ -0,0 +1,253 @@ +// 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.Linq; +using Perspex.Collections; + +namespace Perspex.Controls +{ + /// + /// Holds a collection of style classes for an . + /// + /// + /// Similar to CSS, each control may have any number of styling classes applied. + /// + public class Classes : PerspexList, IPseudoClasses + { + /// + /// Initializes a new instance of the class. + /// + public Classes() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial items. + public Classes(IEnumerable items) + : base(items) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial items. + public Classes(params string[] items) + : base(items) + { + } + + /// + /// Adds a style class to the collection. + /// + /// The class name. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void Add(string name) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + base.Add(name); + } + } + + /// + /// Adds a style classes to the collection. + /// + /// The class names. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void AddRange(IEnumerable names) + { + var c = new List(); + + foreach (var name in names) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + c.Add(name); + } + } + + base.AddRange(c); + } + + /// + /// Inserts a style class into the collection. + /// + /// The index to insert the class at. + /// The class name. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void Insert(int index, string name) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + base.Insert(index, name); + } + } + + /// + /// Inserts style classes into the collection. + /// + /// The index to insert the class at. + /// The class names. + /// + /// Only standard classes may be added via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void InsertRange(int index, IEnumerable names) + { + var c = new List(); + + foreach (var name in names) + { + ThrowIfPseudoclass(name, "added"); + + if (!Contains(name)) + { + c.Add(name); + } + } + + base.InsertRange(index, c); + } + + /// + /// Removes a style class from the collection. + /// + /// The class name. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override bool Remove(string name) + { + ThrowIfPseudoclass(name, "removed"); + return base.Remove(name); + } + + /// + /// Removes style classes from the collection. + /// + /// The class name. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void RemoveAll(IEnumerable names) + { + var c = new List(); + + foreach (var name in names) + { + ThrowIfPseudoclass(name, "removed"); + + if (!Contains(name)) + { + c.Add(name); + } + } + + base.RemoveAll(c); + } + + /// + /// Removes a style class from the collection. + /// + /// The index of the class in the collection. + /// + /// Only standard classes may be removed via this method. To remove pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public override void RemoveAt(int index) + { + var name = this[index]; + ThrowIfPseudoclass(name, "removed"); + base.RemoveAt(index); + } + + /// + /// Removes style classes from the collection. + /// + /// The first index to remove. + /// The number of items to remove. + public override void RemoveRange(int index, int count) + { + var names = GetRange(index, count); + base.RemoveRange(index, count); + } + + /// + /// Removes all non-pseudoclasses in the collection and adds a new set. + /// + /// The new contents of the collection. + public void Replace(IList source) + { + var toRemove = new List(); + + foreach (var name in source) + { + ThrowIfPseudoclass(name, "added"); + } + + foreach (var name in this) + { + if (!name.StartsWith(":")) + { + toRemove.Add(name); + } + } + + base.RemoveAll(toRemove); + base.AddRange(source); + } + + /// + void IPseudoClasses.Add(string name) + { + if (!Contains(name)) + { + base.Add(name); + } + } + + /// + bool IPseudoClasses.Remove(string name) + { + return base.Remove(name); + } + + private void ThrowIfPseudoclass(string name, string operation) + { + if (name.StartsWith(":")) + { + throw new ArgumentException( + $"The pseudoclass '{name}' may only be {operation} by the control itself."); + } + } + } +} diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index c4c1c94e31..e8b52a2948 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using Perspex.Collections; +using System.Linq; +using System.Reactive.Linq; +using Perspex.Controls.Mixins; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; @@ -14,7 +16,7 @@ namespace Perspex.Controls /// /// Displays according to a . /// - public class ContentControl : TemplatedControl, IContentControl, IReparentingHost + public class ContentControl : TemplatedControl, IContentControl { /// /// Defines the property. @@ -35,17 +37,11 @@ namespace Perspex.Controls PerspexProperty.Register(nameof(VerticalContentAlignment)); /// - /// Initializes static members of the class. + /// Initializes static members of the class. /// static ContentControl() { - } - - /// - /// Initializes a new instance of the class. - /// - public ContentControl() - { + ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); } /// @@ -85,31 +81,15 @@ namespace Perspex.Controls set { SetValue(VerticalContentAlignmentProperty, value); } } - /// - /// Gets a writeable logical children collection from the host. - /// - IPerspexList IReparentingHost.LogicalChildren => LogicalChildren; - - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) - { - return control is IContentPresenter && control.TemplatedParent == this; - } - /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { + base.OnTemplateApplied(e); + // We allow ContentControls without ContentPresenters in the template. This can be // useful for e.g. a simple ToggleButton that displays an image. There's no need to // have a ContentPresenter in the visual tree for that. - Presenter = nameScope.Find("PART_ContentPresenter"); + Presenter = e.NameScope.Find("PART_ContentPresenter"); } } } diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index ad959d7f8e..e568b37244 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -2,12 +2,18 @@ // 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.Reactive.Subjects; using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; using Perspex.Interactivity; +using Perspex.LogicalTree; using Perspex.Rendering; using Perspex.Styling; @@ -70,9 +76,11 @@ namespace Perspex.Controls private readonly Classes _classes = new Classes(); private DataTemplates _dataTemplates; private IControl _focusAdorner; + private bool _isAttachedToLogicalTree; private IPerspexList _logicalChildren; private INameScope _nameScope; private Styles _styles; + private Subject _styleDetach = new Subject(); /// /// Initializes static members of the class. @@ -93,6 +101,16 @@ namespace Perspex.Controls _nameScope = this as INameScope; } + /// + /// Raised when the control is attached to a rooted logical tree. + /// + public event EventHandler AttachedToLogicalTree; + + /// + /// Raised when the control is detached from a rooted logical tree. + /// + public event EventHandler DetachedFromLogicalTree; + /// /// Occurs when the property changes. /// @@ -128,8 +146,7 @@ namespace Perspex.Controls { if (_classes != value) { - _classes.Clear(); - _classes.Add(value); + _classes.Replace(value); } } } @@ -206,6 +223,11 @@ namespace Perspex.Controls internal set { SetValue(TemplatedParentProperty, value); } } + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool ILogical.IsAttachedToLogicalTree => _isAttachedToLogicalTree; + /// /// Gets the control's logical parent. /// @@ -216,6 +238,9 @@ namespace Perspex.Controls /// IPerspexReadOnlyList ILogical.LogicalChildren => LogicalChildren; + /// + IPerspexReadOnlyList IStyleable.Classes => Classes; + /// /// Gets the type by which the control is styled. /// @@ -227,9 +252,10 @@ namespace Perspex.Controls /// Type IStyleable.StyleKey => GetType(); - /// - /// Gets the parent style host element. - /// + /// + IObservable IStyleable.StyleDetach => _styleDetach; + + /// IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// @@ -252,14 +278,40 @@ namespace Perspex.Controls if (_logicalChildren == null) { var list = new PerspexList(); - list.ResetBehavior = ResetBehavior.Remove; - _logicalChildren = list; + LogicalChildren = list; } return _logicalChildren; } + + set + { + Contract.Requires(value != null); + + if (_logicalChildren != value) + { + if (_logicalChildren != null) + { + _logicalChildren.CollectionChanged -= LogicalChildrenCollectionChanged; + } + } + + if (value is PerspexList) + { + ((PerspexList)value).ResetBehavior = ResetBehavior.Remove; + } + + _logicalChildren = value; + _logicalChildren.CollectionChanged += LogicalChildrenCollectionChanged; + } } + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + /// /// Sets the control's logical parent. /// @@ -275,7 +327,26 @@ namespace Perspex.Controls throw new InvalidOperationException("The Control already has a parent."); } - SetAndRaise(ParentProperty, ref _parent, (IControl)parent); + InheritanceParent = parent as PerspexObject; + _parent = (IControl)parent; + + var root = FindStyleRoot(old); + + if (root != null) + { + var e = new LogicalTreeAttachmentEventArgs(root); + OnDetachedFromLogicalTree(e); + } + + root = FindStyleRoot(this); + + if (root != null) + { + var e = new LogicalTreeAttachmentEventArgs(root); + OnAttachedToLogicalTree(e); + } + + RaisePropertyChanged(ParentProperty, old, _parent, BindingPriority.LocalValue); } } @@ -316,15 +387,71 @@ namespace Perspex.Controls { if (selector((T)e.NewValue)) { - ((Control)e.Sender).Classes.Add(className); + ((Control)e.Sender).PseudoClasses.Add(className); } else { - ((Control)e.Sender).Classes.Remove(className); + ((Control)e.Sender).PseudoClasses.Remove(className); } }); } + /// + /// Called when the control is added to a logical tree. + /// + /// The event args. + /// + /// It is vital that if you override this method you call the base implementation; + /// failing to do so will cause numerous features to not work as expected. + /// + protected virtual void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_nameScope == null) + { + _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; + } + + if (Name != null) + { + _nameScope?.Register(Name, this); + } + + _isAttachedToLogicalTree = true; + PerspexLocator.Current.GetService()?.ApplyStyles(this); + AttachedToLogicalTree?.Invoke(this, e); + + foreach (var child in LogicalChildren.OfType()) + { + child.OnAttachedToLogicalTree(e); + } + } + + /// + /// Called when the control is removed from a logical tree. + /// + /// The event args. + /// + /// It is vital that if you override this method you call the base implementation; + /// failing to do so will cause numerous features to not work as expected. + /// + protected virtual void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (Name != null) + { + _nameScope?.Unregister(Name); + } + + _isAttachedToLogicalTree = false; + _styleDetach.OnNext(Unit.Default); + this.TemplatedParent = null; + DetachedFromLogicalTree?.Invoke(this, e); + + foreach (var child in LogicalChildren.OfType()) + { + child.OnDetachedFromLogicalTree(e); + } + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -370,35 +497,6 @@ namespace Perspex.Controls } } - /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - - if (_nameScope == null) - { - _nameScope = NameScope.GetNameScope(this) ?? ((Control)Parent)?._nameScope; - } - - if (Name != null) - { - _nameScope?.Register(Name, this); - } - - PerspexLocator.Current.GetService()?.ApplyStyles(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - - if (Name != null) - { - _nameScope?.Unregister(Name); - } - } - /// /// Called when the is changed and all subscribers to that change /// have been notified. @@ -414,7 +512,7 @@ namespace Perspex.Controls /// The logical children to use. protected void RedirectLogicalChildren(IPerspexList collection) { - _logicalChildren = collection; + LogicalChildren = collection; } /// @@ -436,5 +534,66 @@ namespace Perspex.Controls } } } + + private static IStyleRoot FindStyleRoot(IStyleHost e) + { + while (e != null) + { + var root = e as IStyleRoot; + + if (root != null && root.StylingParent == null) + { + return root; + } + + e = e.StylingParent; + } + + return null; + } + + private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + ClearLogicalParent(e.OldItems.Cast()); + break; + + case NotifyCollectionChangedAction.Replace: + ClearLogicalParent(e.OldItems.Cast()); + SetLogicalParent(e.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Reset: + throw new NotSupportedException("Reset should not be signalled on LogicalChildren collection"); + } + } + + private void SetLogicalParent(IEnumerable children) + { + foreach (var i in children) + { + if (i.LogicalParent == null) + { + ((ISetLogicalParent)i).SetParent(this); + } + } + } + + private void ClearLogicalParent(IEnumerable children) + { + foreach (var i in children) + { + if (i.LogicalParent == this) + { + ((ISetLogicalParent)i).SetParent(null); + } + } + } } } diff --git a/src/Perspex.Controls/ControlExtensions.cs b/src/Perspex.Controls/ControlExtensions.cs index 6667810f24..5948331652 100644 --- a/src/Perspex.Controls/ControlExtensions.cs +++ b/src/Perspex.Controls/ControlExtensions.cs @@ -70,5 +70,35 @@ namespace Perspex.Controls .Select(x => (x as INameScope) ?? NameScope.GetNameScope(x)) .FirstOrDefault(x => x != null); } + + /// + /// Adds or removes a pseudoclass depending on a boolean value. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// True to add the pseudoclass or false to remove. + public static void Set(this IPseudoClasses classes, string name, bool value) + { + if (value) + { + classes.Add(name); + } + else + { + classes.Remove(name); + } + } + + /// + /// Sets a pseudoclass depending on an observable trigger. + /// + /// The pseudoclasses collection. + /// The name of the pseudoclass to set. + /// The trigger: true adds the pseudoclass, false removes. + /// A disposable used to cancel the subscription. + public static IDisposable Set(this IPseudoClasses classes, string name, IObservable trigger) + { + return trigger.Subscribe(x => classes.Set(name, x)); + } } } diff --git a/src/Perspex.Controls/Decorator.cs b/src/Perspex.Controls/Decorator.cs index f5da5a4114..3440845c46 100644 --- a/src/Perspex.Controls/Decorator.cs +++ b/src/Perspex.Controls/Decorator.cs @@ -88,13 +88,13 @@ namespace Perspex.Controls { ((ISetLogicalParent)oldChild).SetParent(null); LogicalChildren.Clear(); - RemoveVisualChild(oldChild); + VisualChildren.Remove(oldChild); } if (newChild != null) { ((ISetLogicalParent)newChild).SetParent(this); - AddVisualChild(newChild); + VisualChildren.Add(newChild); LogicalChildren.Add(newChild); } } diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index 31a44ad7d0..7995d48105 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -125,14 +125,14 @@ namespace Perspex.Controls base.OnPointerPressed(e); } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { if (_popup != null) { _popup.Opened -= PopupOpened; } - _popup = nameScope.Get("PART_Popup"); + _popup = e.NameScope.Get("PART_Popup"); _popup.Opened += PopupOpened; } diff --git a/src/Perspex.Controls/Expander.cs b/src/Perspex.Controls/Expander.cs new file mode 100644 index 0000000000..5ac926af47 --- /dev/null +++ b/src/Perspex.Controls/Expander.cs @@ -0,0 +1,74 @@ +using Perspex.Animation; +using Perspex.Controls.Primitives; + +namespace Perspex.Controls +{ + public enum ExpandDirection + { + Down, + Up, + Left, + Right + } + + public class Expander : HeaderedContentControl + { + public static readonly PerspexProperty IsExpandedProperty = + PerspexProperty.Register(nameof(IsExpanded), true); + + public static readonly PerspexProperty ExpandDirectionProperty = + PerspexProperty.Register(nameof(ExpandDirection), ExpandDirection.Down); + + public static readonly PerspexProperty ContentTransitionProperty = + PerspexProperty.Register(nameof(ContentTransition)); + + static Expander() + { + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left"); + PseudoClass(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right"); + + PseudoClass(IsExpandedProperty, ":expanded"); + + IsExpandedProperty.Changed.AddClassHandler(x => x.OnIsExpandedChanged); + } + + public bool IsExpanded + { + get { return GetValue(IsExpandedProperty); } + set { SetValue(IsExpandedProperty, value); } + } + + public ExpandDirection ExpandDirection + { + get { return GetValue(ExpandDirectionProperty); } + set { SetValue(ExpandDirectionProperty, value); } + } + + public IPageTransition ContentTransition + { + get { return GetValue(ContentTransitionProperty); } + set { SetValue(ContentTransitionProperty, value); } + } + + protected virtual void OnIsExpandedChanged(PerspexPropertyChangedEventArgs e) + { + IVisual visualContent = Presenter; + + if (Content != null && ContentTransition != null && visualContent != null) + { + bool forward = ExpandDirection == ExpandDirection.Left || + ExpandDirection == ExpandDirection.Up; + if (IsExpanded) + { + ContentTransition.Start(null, visualContent, forward); + } + else + { + ContentTransition.Start(visualContent, null, !forward); + } + } + } + } +} \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs index 7189d7a0c9..85628e3374 100644 --- a/src/Perspex.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/IItemContainerGenerator.cs @@ -16,12 +16,17 @@ namespace Perspex.Controls.Generators /// /// Gets the currently realized containers. /// - IEnumerable Containers { get; } + IEnumerable Containers { get; } /// - /// Signalled whenever new containers are initialized. + /// Signalled whenever new containers are materialized. /// - IObservable ContainersInitialized { get; } + event EventHandler Materialized; + + /// + /// Event raised whenever containers are dematerialized. + /// + event EventHandler Dematerialized; /// /// Creates container controls for a collection of items. @@ -32,7 +37,7 @@ namespace Perspex.Controls.Generators /// The items. /// An optional member selector. /// The created controls. - IEnumerable Materialize( + IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector); @@ -45,7 +50,7 @@ namespace Perspex.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable Dematerialize(int startingIndex, int count); + IEnumerable Dematerialize(int startingIndex, int count); /// /// Removes a set of created containers and updates the index of later containers to fill @@ -56,13 +61,13 @@ namespace Perspex.Controls.Generators /// /// The the number of items to remove. /// The removed containers. - IEnumerable RemoveRange(int startingIndex, int count); + IEnumerable RemoveRange(int startingIndex, int count); /// /// Clears all created containers and returns the removed controls. /// /// The removed controls. - IEnumerable Clear(); + IEnumerable Clear(); /// /// Gets the container control representing the item with the specified index. diff --git a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs index 37f097618b..fa78433c57 100644 --- a/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ITreeItemContainerGenerator.cs @@ -11,23 +11,8 @@ namespace Perspex.Controls.Generators public interface ITreeItemContainerGenerator : IItemContainerGenerator { /// - /// Gets the item container for the root of the tree, or null if this generator is itself - /// the root of the tree. + /// Gets the container index for the tree. /// - ITreeItemContainerGenerator RootGenerator { get; } - - /// - /// Gets the item container for the specified item, anywhere in the tree. - /// - /// The item. - /// The container, or null if not found. - IControl TreeContainerFromItem(object item); - - /// - /// Gets the item for the specified item container, anywhere in the tree. - /// - /// The container. - /// The item, or null if not found. - object TreeItemFromContainer(IControl container); + TreeContainerIndex Index { get; } } } \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainer.cs b/src/Perspex.Controls/Generators/ItemContainer.cs new file mode 100644 index 0000000000..86f3795106 --- /dev/null +++ b/src/Perspex.Controls/Generators/ItemContainer.cs @@ -0,0 +1,45 @@ +// 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. + +namespace Perspex.Controls.Generators +{ + /// + /// Holds information about an item container generated by an + /// . + /// + public class ItemContainer + { + /// + /// Initializes a new instance of the class. + /// + /// The container control. + /// The item that the container represents. + /// + /// The index of the item in the collection. + /// + public ItemContainer(IControl container, object item, int index) + { + ContainerControl = container; + Item = item; + Index = index; + } + + /// + /// Gets the container control. + /// + /// + /// This will be null if is null. + /// + public IControl ContainerControl { get; } + + /// + /// Gets the item that the container represents. + /// + public object Item { get; } + + /// + /// Gets the index of the item in the collection. + /// + public int Index { get; } + } +} \ No newline at end of file diff --git a/src/Perspex.Controls/Generators/ItemContainers.cs b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs similarity index 62% rename from src/Perspex.Controls/Generators/ItemContainers.cs rename to src/Perspex.Controls/Generators/ItemContainerEventArgs.cs index 9eec0f1147..86e2b890d1 100644 --- a/src/Perspex.Controls/Generators/ItemContainers.cs +++ b/src/Perspex.Controls/Generators/ItemContainerEventArgs.cs @@ -1,34 +1,38 @@ // 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; namespace Perspex.Controls.Generators { /// - /// Holds details about a set of item containers in an . + /// Provides details for the + /// and events. /// - public class ItemContainers + public class ItemContainerEventArgs : EventArgs { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The index of the first container in the source items. /// The containers. - public ItemContainers(int startingIndex, IList containers) + public ItemContainerEventArgs( + int startingIndex, + IList containers) { StartingIndex = startingIndex; - Items = containers; + Containers = containers; } /// - /// Gets the index of the first container in the source items. + /// Gets the containers. /// - public int StartingIndex { get; } + public IList Containers { get; } /// - /// Gets the containers. May contain null entries. + /// Gets the index of the first container in the source items. /// - public IList Items { get; } + public int StartingIndex { get; } } } diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs index b8b57ddd59..2733de5770 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator.cs @@ -15,9 +15,7 @@ namespace Perspex.Controls.Generators /// public class ItemContainerGenerator : IItemContainerGenerator { - private List _containers = new List(); - - private readonly Subject _containersInitialized = new Subject(); + private List _containers = new List(); /// /// Initializes a new instance of the class. @@ -31,10 +29,13 @@ namespace Perspex.Controls.Generators } /// - public IEnumerable Containers => _containers; + public IEnumerable Containers => _containers; + + /// + public event EventHandler Materialized; /// - public IObservable ContainersInitialized => _containersInitialized; + public event EventHandler Dematerialized; /// /// Gets the owner control. @@ -42,7 +43,7 @@ namespace Perspex.Controls.Generators public IControl Owner { get; } /// - public IEnumerable Materialize( + public IEnumerable Materialize( int startingIndex, IEnumerable items, IMemberSelector selector) @@ -50,25 +51,25 @@ namespace Perspex.Controls.Generators Contract.Requires(items != null); int index = startingIndex; - var result = new List(); + var result = new List(); foreach (var item in items) { var i = selector != null ? selector.Select(item) : item; - var container = CreateContainer(i); + var container = new ItemContainer(CreateContainer(i), item, index++); result.Add(container); } - AddContainers(startingIndex, result); - _containersInitialized.OnNext(new ItemContainers(startingIndex, result)); + AddContainers(result); + Materialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); return result.Where(x => x != null).ToList(); } /// - public virtual IEnumerable Dematerialize(int startingIndex, int count) + public virtual IEnumerable Dematerialize(int startingIndex, int count) { - var result = new List(); + var result = new List(); for (int i = startingIndex; i < startingIndex + count; ++i) { @@ -79,22 +80,31 @@ namespace Perspex.Controls.Generators } } + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); + return result; } /// - public virtual IEnumerable RemoveRange(int startingIndex, int count) + public virtual IEnumerable RemoveRange(int startingIndex, int count) { var result = _containers.GetRange(startingIndex, count); _containers.RemoveRange(startingIndex, count); + Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); return result; } /// - public virtual IEnumerable Clear() + public virtual IEnumerable Clear() { var result = _containers; - _containers = new List(); + _containers = new List(); + + if (result.Count > 0) + { + Dematerialized?.Invoke(this, new ItemContainerEventArgs(0, result)); + } + return result; } @@ -103,7 +113,7 @@ namespace Perspex.Controls.Generators { if (index < _containers.Count) { - return _containers[index]; + return _containers[index]?.ContainerControl; } return null; @@ -112,7 +122,19 @@ namespace Perspex.Controls.Generators /// public int IndexFromContainer(IControl container) { - return _containers.IndexOf(container); + var index = 0; + + foreach (var i in _containers) + { + if (i?.ContainerControl == container) + { + return index; + } + + ++index; + } + + return -1; } /// @@ -135,33 +157,30 @@ namespace Perspex.Controls.Generators /// /// Adds a collection of containers to the index. /// - /// The starting index. - /// The container. - protected void AddContainers(int index, IList container) + /// The containers. + protected void AddContainers(IList containers) { - Contract.Requires(container != null); + Contract.Requires(containers != null); - foreach (var c in container) + foreach (var c in containers) { - while (_containers.Count < index) + while (_containers.Count < c.Index) { _containers.Add(null); } - if (_containers.Count == index) + if (_containers.Count == c.Index) { _containers.Add(c); } - else if (_containers[index] == null) + else if (_containers[c.Index] == null) { - _containers[index] = c; + _containers[c.Index] = c; } else { throw new InvalidOperationException("Container already created."); } - - ++index; } } @@ -171,7 +190,7 @@ namespace Perspex.Controls.Generators /// The first index. /// The number of elements in the range. /// The containers. - protected IEnumerable GetContainerRange(int index, int count) + protected IEnumerable GetContainerRange(int index, int count) { return _containers.GetRange(index, count); } diff --git a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs index dcb365b120..1a18c2d154 100644 --- a/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Perspex.Controls/Generators/ItemContainerGenerator`1.cs @@ -24,6 +24,9 @@ namespace Perspex.Controls.Generators PerspexProperty contentProperty) : base(owner) { + Contract.Requires(owner != null); + Contract.Requires(contentProperty != null); + ContentProperty = contentProperty; } @@ -48,7 +51,7 @@ namespace Perspex.Controls.Generators else { var result = new T(); - result.SetValue(ContentProperty, Owner.MaterializeDataTemplate(item)); + result.SetValue(ContentProperty, item); if (!(item is IControl)) { diff --git a/src/Perspex.Controls/Generators/TreeContainerIndex.cs b/src/Perspex.Controls/Generators/TreeContainerIndex.cs new file mode 100644 index 0000000000..9de4ca1050 --- /dev/null +++ b/src/Perspex.Controls/Generators/TreeContainerIndex.cs @@ -0,0 +1,87 @@ +// 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.Collections.Generic; + +namespace Perspex.Controls.Generators +{ + /// + /// Maintains an index of all item containers currently materialized by a . + /// + /// + /// Each has its own + /// that maintains the list of its direct children, but they also share an instance of this + /// class in their property which tracks + /// the containers materialized for the entire tree. + /// + public class TreeContainerIndex + { + private readonly Dictionary _itemToContainer = new Dictionary(); + private readonly Dictionary _containerToItem = new Dictionary(); + + /// + /// Gets the currently materialized containers. + /// + public IEnumerable Items => _containerToItem.Keys; + + /// + /// Adds an entry to the index. + /// + /// The item. + /// The item container. + public void Add(object item, IControl container) + { + _itemToContainer.Add(item, container); + _containerToItem.Add(container, item); + } + + /// + /// Removes a container from the index. + /// + /// The item container. + public void Remove(IControl container) + { + var item = _containerToItem[container]; + _containerToItem.Remove(container); + _itemToContainer.Remove(item); + } + + /// + /// Removes a set of containers from the index. + /// + /// The item containers. + public void Remove(IEnumerable containers) + { + foreach (var container in containers) + { + var item = _containerToItem[container.ContainerControl]; + _containerToItem.Remove(container.ContainerControl); + _itemToContainer.Remove(item); + } + } + + /// + /// Gets the container for an item. + /// + /// The item. + /// The container, or null of not found. + public IControl ContainerFromItem(object item) + { + IControl result; + _itemToContainer.TryGetValue(item, out result); + return result; + } + + /// + /// Gets the item for a container. + /// + /// The container. + /// The item, or null of not found. + public object ItemFromContainer(IControl container) + { + object result; + _containerToItem.TryGetValue(container, out result); + return result; + } + } +} diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 802cd3866b..e630924a97 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Generic; using Perspex.Controls.Templates; @@ -14,9 +15,6 @@ namespace Perspex.Controls.Generators public class TreeItemContainerGenerator : ItemContainerGenerator, ITreeItemContainerGenerator where T : class, IControl, new() { - private readonly Dictionary _itemToContainer; - private readonly Dictionary _containerToItem; - /// /// Initializes a new instance of the class. /// @@ -24,34 +22,30 @@ namespace Perspex.Controls.Generators /// The container's Content property. /// The container's Items property. /// The container's IsExpanded property. - /// - /// The item container for the root of the tree, or null if this generator is itself the - /// root of the tree. - /// + /// The container index for the tree public TreeItemContainerGenerator( IControl owner, PerspexProperty contentProperty, PerspexProperty itemsProperty, PerspexProperty isExpandedProperty, - ITreeItemContainerGenerator rootGenerator) + TreeContainerIndex index) : base(owner, contentProperty) { + Contract.Requires(owner != null); + Contract.Requires(contentProperty != null); + Contract.Requires(itemsProperty != null); + Contract.Requires(isExpandedProperty != null); + Contract.Requires(index != null); + ItemsProperty = itemsProperty; IsExpandedProperty = isExpandedProperty; - RootGenerator = rootGenerator; - - if (rootGenerator == null) - { - _itemToContainer = new Dictionary(); - _containerToItem = new Dictionary(); - } + Index = index; } /// - /// Gets the item container for the root of the tree, or null if this generator is itself - /// the root of the tree. + /// Gets the container index for the tree. /// - public ITreeItemContainerGenerator RootGenerator { get; } + public TreeContainerIndex Index { get; } /// /// Gets the item container's Items property. @@ -63,30 +57,6 @@ namespace Perspex.Controls.Generators /// protected PerspexProperty IsExpandedProperty { get; } - /// - /// Gets the item container for the specified item, anywhere in the tree. - /// - /// The item. - /// The container, or null if not found. - public IControl TreeContainerFromItem(object item) - { - T result; - _itemToContainer.TryGetValue(item, out result); - return result; - } - - /// - /// Gets the item for the specified item container, anywhere in the tree. - /// - /// The container. - /// The item, or null if not found. - public object TreeItemFromContainer(IControl container) - { - object result; - _containerToItem.TryGetValue(container, out result); - return result; - } - /// protected override IControl CreateContainer(object item) { @@ -114,65 +84,29 @@ namespace Perspex.Controls.Generators result.DataContext = item; } - AddToIndex(item, result); + Index.Add(item, result); return result; } } - public override IEnumerable Clear() + public override IEnumerable Clear() { - ClearIndex(); - return base.Clear(); + var items = base.Clear(); + Index.Remove(items); + return items; } - public override IEnumerable Dematerialize(int startingIndex, int count) + public override IEnumerable Dematerialize(int startingIndex, int count) { - RemoveFromIndex(GetContainerRange(startingIndex, count)); + Index.Remove(GetContainerRange(startingIndex, count)); return base.Dematerialize(startingIndex, count); } - private void AddToIndex(object item, T container) + public override IEnumerable RemoveRange(int startingIndex, int count) { - if (RootGenerator != null) - { - ((TreeItemContainerGenerator)RootGenerator).AddToIndex(item, container); - } - else - { - _itemToContainer.Add(item, container); - _containerToItem.Add(container, item); - } - } - - private void RemoveFromIndex(IEnumerable containers) - { - if (RootGenerator != null) - { - ((TreeItemContainerGenerator)RootGenerator).RemoveFromIndex(containers); - } - else - { - foreach (var container in containers) - { - var item = _containerToItem[container]; - _containerToItem.Remove(container); - _itemToContainer.Remove(item); - } - } - } - - private void ClearIndex() - { - if (RootGenerator != null) - { - ((TreeItemContainerGenerator)RootGenerator).ClearIndex(); - } - else - { - _containerToItem.Clear(); - _itemToContainer.Clear(); - } + Index.Remove(GetContainerRange(startingIndex, count)); + return base.RemoveRange(startingIndex, count); } /// diff --git a/src/Perspex.Controls/IControl.cs b/src/Perspex.Controls/IControl.cs index 21cb044d0a..0e65c599be 100644 --- a/src/Perspex.Controls/IControl.cs +++ b/src/Perspex.Controls/IControl.cs @@ -13,6 +13,11 @@ namespace Perspex.Controls /// public interface IControl : IVisual, ILogical, ILayoutable, IInputElement, INamed, IStyleable, IStyleHost { + /// + /// Gets or sets the control's styling classes. + /// + new Classes Classes { get; set; } + /// /// Gets or sets the control's data context. /// diff --git a/src/Perspex.Controls/IPseudoClasses.cs b/src/Perspex.Controls/IPseudoClasses.cs new file mode 100644 index 0000000000..87e5a9259a --- /dev/null +++ b/src/Perspex.Controls/IPseudoClasses.cs @@ -0,0 +1,25 @@ +// 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.Controls +{ + /// + /// Exposes an interface for setting pseudoclasses on a collection. + /// + public interface IPseudoClasses + { + /// + /// Adds a pseudoclass to the collection. + /// + /// The pseudoclass name. + void Add(string name); + + /// + /// Removes a pseudoclass from the collection. + /// + /// The pseudoclass name. + bool Remove(string name); + } +} diff --git a/src/Perspex.Controls/IReparentingControl.cs b/src/Perspex.Controls/IReparentingControl.cs deleted file mode 100644 index a7d4b46e64..0000000000 --- a/src/Perspex.Controls/IReparentingControl.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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 Perspex.Collections; - -namespace Perspex.Controls -{ - /// - /// A control that can make its visual children the logical children of another control. - /// - public interface IReparentingControl : IControl - { - /// - /// Requests that the visual children of the control use another control as their logical - /// parent. - /// - /// - /// The logical parent for the visual children of the control. - /// - /// - /// The collection to modify. - /// - void ReparentLogicalChildren(ILogical logicalParent, IPerspexList children); - } -} \ No newline at end of file diff --git a/src/Perspex.Controls/IReparentingHost.cs b/src/Perspex.Controls/IReparentingHost.cs deleted file mode 100644 index 99dcf7d4f1..0000000000 --- a/src/Perspex.Controls/IReparentingHost.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 Perspex.Collections; - -namespace Perspex.Controls -{ - /// - /// A control that can use the visual children of another control as its logical children. - /// - public interface IReparentingHost : ILogical - { - /// - /// Gets a writeable logical children collection from the host. - /// - new IPerspexList LogicalChildren { get; } - - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool WillReparentChildrenOf(IControl control); - } -} \ No newline at end of file diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 5c06dbc04e..16873b7f05 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -3,7 +3,7 @@ using System; using System.Collections; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -21,7 +21,7 @@ namespace Perspex.Controls /// /// Displays a collection of items. /// - public class ItemsControl : TemplatedControl, IReparentingHost + public class ItemsControl : TemplatedControl { /// /// The default value for the property. @@ -64,7 +64,7 @@ namespace Perspex.Controls /// public ItemsControl() { - Classes.Add(":empty"); + PseudoClasses.Add(":empty"); SubscribeToItems(_items); } @@ -78,6 +78,12 @@ namespace Perspex.Controls if (_itemContainerGenerator == null) { _itemContainerGenerator = CreateItemContainerGenerator(); + + if (_itemContainerGenerator != null) + { + _itemContainerGenerator.Materialized += (_, e) => OnContainersMaterialized(e); + _itemContainerGenerator.Dematerialized += (_, e) => OnContainersDematerialized(e); + } } return _itemContainerGenerator; @@ -118,38 +124,141 @@ namespace Perspex.Controls public IItemsPresenter Presenter { get; - set; + protected set; } - /// - IPerspexList IReparentingHost.LogicalChildren => LogicalChildren; + /// + /// Gets the item at the specified index in a collection. + /// + /// The collection. + /// The index. + /// The index of the item or -1 if the item was not found. + protected static object ElementAt(IEnumerable items, int index) + { + var typedItems = items?.Cast(); + + if (index != -1 && typedItems != null && index < typedItems.Count()) + { + return typedItems.ElementAt(index) ?? null; + } + else + { + return null; + } + } /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. + /// Gets the index of an item in a collection. /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) + /// The collection. + /// The item. + /// The index of the item or -1 if the item was not found. + protected static int IndexOf(IEnumerable items, object item) { - return control is IItemsPresenter && control.TemplatedParent == this; + if (items != null && item != null) + { + var list = items as IList; + + if (list != null) + { + return list.IndexOf(item); + } + else + { + int index = 0; + + foreach (var i in items) + { + if (Equals(i, item)) + { + return index; + } + + ++index; + } + } + } + + return -1; } /// /// Creates the for the control. /// - /// An . + /// + /// An or null. + /// + /// + /// Certain controls such as don't actually create item + /// containers; however they want it to be ItemsControls so that they have an Items + /// property etc. In this case, a derived class can override this method to return null + /// in order to disable the creation of item containers. + /// protected virtual IItemContainerGenerator CreateItemContainerGenerator() { return new ItemContainerGenerator(this); } + /// + /// Called when new containers are materialized for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersMaterialized(ItemContainerEventArgs e) + { + var toAdd = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be added to the logical tree when + // it was added to the Items collection. + if (container.ContainerControl != container.Item) + { + toAdd.Add(container.ContainerControl); + } + } + + LogicalChildren.AddRange(toAdd); + } + + /// + /// Called when containers are dematerialized for the by its + /// . + /// + /// The details of the containers. + protected virtual void OnContainersDematerialized(ItemContainerEventArgs e) + { + var toRemove = new List(); + + foreach (var container in e.Containers) + { + // If the item is its own container, then it will be removed from the logical tree + // when it is removed from the Items collection. + if (container.ContainerControl != container.Item) + { + toRemove.Add(container.ContainerControl); + } + } + + LogicalChildren.RemoveAll(toRemove); + } + /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - Presenter = nameScope.Find("PART_ItemsPresenter"); + base.OnTemplateApplied(e); + Presenter = e.NameScope.Find("PART_ItemsPresenter"); + } + + /// + protected override void OnTemplateChanged(PerspexPropertyChangedEventArgs e) + { + base.OnTemplateChanged(e); + + if (e.NewValue == null) + { + ItemContainerGenerator?.Clear(); + } } /// @@ -165,7 +274,11 @@ namespace Perspex.Controls incc.CollectionChanged -= ItemsCollectionChanged; } + var oldValue = e.OldValue as IEnumerable; var newValue = e.NewValue as IEnumerable; + + RemoveControlItemsFromLogicalChildren(oldValue); + AddControlItemsToLogicalChildren(newValue); SubscribeToItems(newValue); } @@ -177,16 +290,67 @@ namespace Perspex.Controls /// The event args. protected virtual void ItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + AddControlItemsToLogicalChildren(e.NewItems); + break; + + case NotifyCollectionChangedAction.Remove: + RemoveControlItemsFromLogicalChildren(e.OldItems); + break; + } + var collection = sender as ICollection; + PseudoClasses.Set(":empty", collection.Count == 0); + } + + /// + /// Given a collection of items, adds those that are controls to the logical children. + /// + /// The items. + private void AddControlItemsToLogicalChildren(IEnumerable items) + { + var toAdd = new List(); - if (collection.Count == 0) + if (items != null) { - Classes.Add(":empty"); + foreach (var i in items) + { + var control = i as IControl; + + if (control != null && !LogicalChildren.Contains(control)) + { + toAdd.Add(control); + } + } } - else + + LogicalChildren.AddRange(toAdd); + } + + /// + /// Given a collection of items, removes those that are controls to from logical children. + /// + /// The items. + private void RemoveControlItemsFromLogicalChildren(IEnumerable items) + { + var toRemove = new List(); + + if (items != null) { - Classes.Remove(":empty"); + foreach (var i in items) + { + var control = i as IControl; + + if (control != null) + { + toRemove.Add(control); + } + } } + + LogicalChildren.RemoveAll(toRemove); } /// @@ -195,14 +359,7 @@ namespace Perspex.Controls /// private void SubscribeToItems(IEnumerable items) { - if (items == null || items.Count() == 0) - { - Classes.Add(":empty"); - } - else - { - Classes.Remove(":empty"); - } + PseudoClasses.Set(":empty", items == null || items.Count() == 0); var incc = items as INotifyCollectionChanged; diff --git a/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs b/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs new file mode 100644 index 0000000000..024942cf71 --- /dev/null +++ b/src/Perspex.Controls/LogicalTreeAttachmentEventArgs.cs @@ -0,0 +1,31 @@ +// 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.Styling; + +namespace Perspex.Controls +{ + /// + /// Holds the event arguments for the and + /// events. + /// + public class LogicalTreeAttachmentEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The root of the logical tree. + public LogicalTreeAttachmentEventArgs(IStyleHost root) + { + Contract.Requires(root != null); + + Root = root; + } + + /// + /// Gets the root of the logical tree that the control is being attached to or detached from. + /// + public IStyleHost Root { get; } + } +} diff --git a/src/Perspex.Controls/MenuItem.cs b/src/Perspex.Controls/MenuItem.cs index 7207bfa808..1a95b48a41 100644 --- a/src/Perspex.Controls/MenuItem.cs +++ b/src/Perspex.Controls/MenuItem.cs @@ -20,7 +20,7 @@ namespace Perspex.Controls /// /// A menu item control. /// - public class MenuItem : SelectingItemsControl, ISelectable + public class MenuItem : HeaderedSelectingItemsControl, ISelectable { /// /// Defines the property. @@ -40,12 +40,6 @@ namespace Perspex.Controls public static readonly PerspexProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly PerspexProperty HeaderProperty = - HeaderedItemsControl.HeaderProperty.AddOwner(); - /// /// Defines the property. /// @@ -136,7 +130,6 @@ namespace Perspex.Controls set { SetValue(CommandProperty, value); } } - /// /// Gets or sets an associated with this control /// @@ -156,15 +149,6 @@ namespace Perspex.Controls set { SetValue(CommandParameterProperty, value); } } - /// - /// Gets or sets the 's header. - /// - public object Header - { - get { return GetValue(HeaderProperty); } - set { SetValue(HeaderProperty, value); } - } - /// /// Gets or sets the icon that appears in a . /// @@ -370,14 +354,12 @@ namespace Perspex.Controls } } - /// - /// Called when the MenuItem's template has been applied. - /// - protected override void OnTemplateApplied(INameScope nameScope) + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(nameScope); + base.OnTemplateApplied(e); - _popup = nameScope.Get("PART_Popup"); + _popup = e.NameScope.Get("PART_Popup"); _popup.DependencyResolver = DependencyResolver.Instance; _popup.PopupRootCreated += PopupRootCreated; _popup.Opened += PopupOpened; diff --git a/src/Perspex.Controls/Mixins/ContentControlMixin.cs b/src/Perspex.Controls/Mixins/ContentControlMixin.cs new file mode 100644 index 0000000000..65153ed0af --- /dev/null +++ b/src/Perspex.Controls/Mixins/ContentControlMixin.cs @@ -0,0 +1,136 @@ +// 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 System.Runtime.CompilerServices; +using Perspex.Collections; +using Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Interactivity; + +namespace Perspex.Controls.Mixins +{ + /// + /// Adds content control functionality to control classes. + /// + /// + /// The adds behavior to a control which acts as a content + /// control such as and . It + /// updates keeps the control's logical children in sync with the content being displayed by + /// the control. + /// + public class ContentControlMixin + { + private static Lazy> subscriptions = + new Lazy>(() => + new ConditionalWeakTable()); + + /// + /// Initializes a new instance of the class. + /// + /// The control type. + /// The content property. + /// + /// Given an control of should return the control's + /// logical children collection. + /// + /// + /// The name of the content presenter in the control's template. + /// + public static void Attach( + PerspexProperty content, + Func> logicalChildrenSelector, + string presenterName = "PART_ContentPresenter") + where TControl : TemplatedControl + { + Contract.Requires(content != null); + Contract.Requires(logicalChildrenSelector != null); + + EventHandler templateApplied = (s, ev) => + { + var sender = s as TControl; + + if (sender != null) + { + var e = (TemplateAppliedEventArgs)ev; + var presenter = (IControl)e.NameScope.Find(presenterName); + + if (presenter != null) + { + var logicalChildren = logicalChildrenSelector(sender); + var subscription = presenter + .GetObservable(ContentPresenter.ChildProperty) + .Subscribe(child => UpdateLogicalChild( + logicalChildren, + logicalChildren.FirstOrDefault(), + child)); + subscriptions.Value.Add(sender, subscription); + } + } + }; + + TemplatedControl.TemplateAppliedEvent.AddClassHandler( + typeof(TControl), + templateApplied, + RoutingStrategies.Direct); + + content.Changed.Subscribe(e => + { + var sender = e.Sender as TControl; + + if (sender != null) + { + var logicalChildren = logicalChildrenSelector(sender); + UpdateLogicalChild(logicalChildren, e.OldValue, e.NewValue); + } + }); + + TemplatedControl.TemplateProperty.Changed.Subscribe(e => + { + var sender = e.Sender as TControl; + + if (sender != null) + { + IDisposable subscription; + + if (subscriptions.Value.TryGetValue(sender, out subscription)) + { + subscription.Dispose(); + subscriptions.Value.Remove(sender); + } + } + }); + } + + private static event EventHandler TemplateApplied; + + private static void OnTemplateApplied(object sender, RoutedEventArgs e) + { + TemplateApplied?.Invoke(sender, (TemplateAppliedEventArgs)e); + } + + private static void UpdateLogicalChild( + IPerspexList logicalChildren, + object oldValue, + object newValue) + { + if (oldValue != newValue) + { + var logical = oldValue as ILogical; + + if (logical != null) + { + logicalChildren.Remove(logical); + } + + logical = newValue as ILogical; + + if (logical != null) + { + logicalChildren.Add(logical); + } + } + } + } +} diff --git a/src/Perspex.Controls/Mixins/SelectableMixin.cs b/src/Perspex.Controls/Mixins/SelectableMixin.cs index 75abe9a849..21b04f9702 100644 --- a/src/Perspex.Controls/Mixins/SelectableMixin.cs +++ b/src/Perspex.Controls/Mixins/SelectableMixin.cs @@ -52,7 +52,7 @@ namespace Perspex.Controls.Mixins { if ((bool)x.NewValue) { - sender.Classes.Add(":selected"); + ((IPseudoClasses)sender.Classes).Add(":selected"); if (((IVisual)sender).IsAttachedToVisualTree) { @@ -61,7 +61,7 @@ namespace Perspex.Controls.Mixins } else { - sender.Classes.Remove(":selected"); + ((IPseudoClasses)sender.Classes).Remove(":selected"); } sender.RaiseEvent(new RoutedEventArgs diff --git a/src/Perspex.Controls/Panel.cs b/src/Perspex.Controls/Panel.cs index 9adda8bb16..edb110f22d 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.Collections; using Perspex.Media; using Perspex.Metadata; @@ -18,7 +17,7 @@ namespace Perspex.Controls /// Controls can be added to a by adding them to its /// collection. All children are layed out to fill the panel. /// - public class Panel : Control, IReparentingControl, IPanel + public class Panel : Control, IPanel { /// /// Defines the property. @@ -68,7 +67,7 @@ namespace Perspex.Controls { Contract.Requires(value != null); - ClearVisualChildren(); + VisualChildren.Clear(); _children.Clear(); _children.AddRange(value); } @@ -83,58 +82,6 @@ namespace Perspex.Controls set { SetValue(BackgroundProperty, value); } } - /// - /// Requests that the visual children of the panel use another control as their logical - /// parent. - /// - /// - /// The logical parent for the visual children of the panel. - /// - /// - /// The collection to modify. - /// - void IReparentingControl.ReparentLogicalChildren(ILogical logicalParent, IPerspexList children) - { - Contract.Requires(logicalParent != null); - Contract.Requires(children != null); - - _childLogicalParent = logicalParent; - RedirectLogicalChildren(children); - - foreach (var control in Children) - { - ((ISetLogicalParent)control).SetParent(null); - ((ISetLogicalParent)control).SetParent((IControl)logicalParent); - children.Add(control); - } - } - - /// - /// Clears for the specified controls. - /// - /// The controls. - private void ClearLogicalParent(IEnumerable controls) - { - foreach (var control in controls) - { - ((ISetLogicalParent)control).SetParent(null); - } - } - - /// - /// Sets for the specified controls. - /// - /// The controls. - private void SetLogicalParent(IEnumerable controls) - { - var parent = _childLogicalParent as Control; - - foreach (var control in controls) - { - ((ISetLogicalParent)control).SetParent(parent); - } - } - /// /// Called when the collection changes. /// @@ -144,29 +91,35 @@ namespace Perspex.Controls { List controls; - // TODO: Handle Replace. switch (e.Action) { case NotifyCollectionChangedAction.Add: controls = e.NewItems.OfType().ToList(); - SetLogicalParent(controls); - AddVisualChildren(e.NewItems.OfType()); LogicalChildren.InsertRange(e.NewStartingIndex, controls); + VisualChildren.AddRange(e.NewItems.OfType()); break; case NotifyCollectionChangedAction.Remove: controls = e.OldItems.OfType().ToList(); - ClearLogicalParent(e.OldItems.OfType()); LogicalChildren.RemoveAll(controls); - RemoveVisualChildren(e.OldItems.OfType()); + VisualChildren.RemoveAll(e.OldItems.OfType()); + break; + + case NotifyCollectionChangedAction.Replace: + for (var i = 0; i < e.OldItems.Count; ++i) + { + var index = i + e.OldStartingIndex; + var child = (IControl)e.NewItems[i]; + LogicalChildren[index] = child; + VisualChildren[index] = child; + } break; case NotifyCollectionChangedAction.Reset: controls = e.OldItems.OfType().ToList(); - ClearLogicalParent(controls); LogicalChildren.Clear(); - ClearVisualChildren(); - AddVisualChildren(_children); + VisualChildren.Clear(); + VisualChildren.AddRange(_children); break; } diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 5a8557da6b..c0fbb8fab3 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -42,24 +42,33 @@ Properties\SharedAssemblyInfo.cs + + + + + + + + + + - + - @@ -71,14 +80,11 @@ - - - @@ -149,6 +155,7 @@ + diff --git a/src/Perspex.Controls/Presenters/CarouselPresenter.cs b/src/Perspex.Controls/Presenters/CarouselPresenter.cs index e130936d29..cee9a053db 100644 --- a/src/Perspex.Controls/Presenters/CarouselPresenter.cs +++ b/src/Perspex.Controls/Presenters/CarouselPresenter.cs @@ -162,20 +162,13 @@ namespace Perspex.Controls.Presenters /// private void CreatePanel() { - var logicalHost = this.FindReparentingHost(); - - ClearVisualChildren(); Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); - AddVisualChild(Panel); - - if (logicalHost != null) - { - ((IReparentingControl)Panel).ReparentLogicalChildren( - logicalHost, - logicalHost.LogicalChildren); - } + LogicalChildren.Clear(); + VisualChildren.Clear(); + LogicalChildren.Add(Panel); + VisualChildren.Add(Panel); _createdPanel = true; var task = MoveToPage(-1, SelectedIndex); @@ -204,7 +197,8 @@ namespace Perspex.Controls.Presenters { var item = Items.Cast().ElementAt(toIndex); to = generator.ContainerFromIndex(toIndex) ?? - generator.Materialize(toIndex, new[] { item }, MemberSelector).FirstOrDefault(); + generator.Materialize(toIndex, new[] { item }, MemberSelector) + .FirstOrDefault()?.ContainerControl; if (to != null) { diff --git a/src/Perspex.Controls/Presenters/ContentPresenter.cs b/src/Perspex.Controls/Presenters/ContentPresenter.cs index 46a57dd613..0faa7fd783 100644 --- a/src/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ContentPresenter.cs @@ -63,6 +63,14 @@ namespace Perspex.Controls.Presenters } } + /// + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + _createdChild = false; + InvalidateMeasure(); + } + /// protected override Size MeasureCore(Size availableSize) { @@ -101,18 +109,12 @@ namespace Perspex.Controls.Presenters var old = Child; var content = Content; var result = this.MaterializeDataTemplate(content); - var logicalHost = this.FindReparentingHost(); - var logicalChildren = logicalHost?.LogicalChildren ?? LogicalChildren; if (old != null) { - ((ISetLogicalParent)old).SetParent(null); - logicalChildren.Remove(old); - ClearVisualChildren(); + VisualChildren.Remove(old); } - Child = result; - if (result != null) { if (!(content is IControl)) @@ -120,14 +122,12 @@ namespace Perspex.Controls.Presenters result.DataContext = content; } - if (result.Parent == null) - { - ((ISetLogicalParent)result).SetParent((ILogical)logicalHost ?? this); - } - - AddVisualChild(result); - logicalChildren.Remove(old); - logicalChildren.Add(result); + Child = result; + VisualChildren.Add(result); + } + else + { + Child = null; } _createdChild = true; diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index ecd74e7bdb..6f052af7f7 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Perspex.Controls.Generators; using Perspex.Controls.Templates; using Perspex.Input; @@ -150,7 +151,6 @@ namespace Perspex.Controls.Presenters /// private void CreatePanel() { - ClearVisualChildren(); Panel = ItemsPanel.Build(); Panel.SetValue(TemplatedParentProperty, TemplatedParent); @@ -161,16 +161,10 @@ namespace Perspex.Controls.Presenters KeyboardNavigationMode.Contained); } - AddVisualChild(Panel); - - var logicalHost = this.FindReparentingHost(); - - if (logicalHost != null) - { - ((IReparentingControl)Panel).ReparentLogicalChildren( - logicalHost, - logicalHost.LogicalChildren); - } + LogicalChildren.Clear(); + VisualChildren.Clear(); + LogicalChildren.Add(Panel); + VisualChildren.Add(Panel); KeyboardNavigation.SetTabNavigation( (InputElement)Panel, @@ -187,7 +181,7 @@ namespace Perspex.Controls.Presenters { if (items != null) { - Panel.Children.AddRange(ItemContainerGenerator.Materialize(0, Items, MemberSelector)); + AddContainers(ItemContainerGenerator.Materialize(0, Items, MemberSelector)); INotifyCollectionChanged incc = items as INotifyCollectionChanged; @@ -238,30 +232,28 @@ namespace Perspex.Controls.Presenters if (_createdPanel) { var generator = ItemContainerGenerator; - IEnumerable containers; // TODO: Handle Move and Replace etc. switch (e.Action) { case NotifyCollectionChangedAction.Add: - containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); - Panel.Children.AddRange(containers); + AddContainers(generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector)); break; case NotifyCollectionChangedAction.Remove: - containers = generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count); - Panel.Children.RemoveAll(containers); + RemoveContainers(generator.RemoveRange(e.OldStartingIndex, e.OldItems.Count)); break; case NotifyCollectionChangedAction.Replace: - generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count); - containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + RemoveContainers(generator.Dematerialize(e.OldStartingIndex, e.OldItems.Count)); + var containers = generator.Materialize(e.NewStartingIndex, e.NewItems, MemberSelector); + AddContainers(containers); var i = e.NewStartingIndex; foreach (var container in containers) { - Panel.Children[i++] = container; + Panel.Children[i++] = container.ContainerControl; } break; @@ -269,13 +261,35 @@ namespace Perspex.Controls.Presenters case NotifyCollectionChangedAction.Move: // TODO: Implement Move in a more efficient manner. case NotifyCollectionChangedAction.Reset: - Panel.Children.RemoveAll(generator.Clear()); - Panel.Children.AddRange(generator.Materialize(0, Items, MemberSelector)); + RemoveContainers(generator.Clear()); + AddContainers(generator.Materialize(0, Items, MemberSelector)); break; } InvalidateMeasure(); } } + + private void AddContainers(IEnumerable items) + { + foreach (var i in items) + { + if (i.ContainerControl != null) + { + this.Panel.Children.Add(i.ContainerControl); + } + } + } + + private void RemoveContainers(IEnumerable items) + { + foreach (var i in items) + { + if (i.ContainerControl != null) + { + this.Panel.Children.Remove(i.ContainerControl); + } + } + } } } diff --git a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs index 65aa4ae4f9..1ea8b3ef41 100644 --- a/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ScrollContentPresenter.cs @@ -3,6 +3,9 @@ using System; using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Layout; using Perspex.VisualTree; @@ -39,6 +42,7 @@ namespace Perspex.Controls.Presenters PerspexProperty.Register("CanScrollHorizontally", true); private Size _measuredExtent; + private IDisposable _scrollableSubscription; /// /// Initializes static members of the class. @@ -56,6 +60,8 @@ namespace Perspex.Controls.Presenters public ScrollContentPresenter() { AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested); + + GetObservable(ChildProperty).Subscribe(ChildChanged); } /// @@ -149,19 +155,24 @@ namespace Perspex.Controls.Presenters /// protected override Size MeasureOverride(Size availableSize) { - var content = Content as ILayoutable; + var child = Child; - if (content != null) + if (child != null) { - var measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + var measureSize = availableSize; - if (!CanScrollHorizontally) + if (_scrollableSubscription == null) { - measureSize = measureSize.WithWidth(availableSize.Width); + measureSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + + if (!CanScrollHorizontally) + { + measureSize = measureSize.WithWidth(availableSize.Width); + } } - content.Measure(measureSize); - var size = content.DesiredSize; + child.Measure(measureSize); + var size = child.DesiredSize; _measuredExtent = size; return size.Constrain(availableSize); } @@ -175,16 +186,21 @@ namespace Perspex.Controls.Presenters protected override Size ArrangeOverride(Size finalSize) { var child = this.GetVisualChildren().SingleOrDefault() as ILayoutable; + var offset = default(Vector); - Viewport = finalSize; - Extent = _measuredExtent; + if (_scrollableSubscription == null) + { + Viewport = finalSize; + Extent = _measuredExtent; + offset = Offset; + } if (child != null) { var size = new Size( Math.Max(finalSize.Width, child.DesiredSize.Width), Math.Max(finalSize.Height, child.DesiredSize.Height)); - child.Arrange(new Rect((Point)(-Offset), size)); + child.Arrange(new Rect((Point)(-offset), size)); return finalSize; } @@ -209,6 +225,30 @@ namespace Perspex.Controls.Presenters e.Handled = BringDescendentIntoView(e.TargetObject, e.TargetRect); } + private void ChildChanged(IControl child) + { + var scrollable = child as IScrollable; + + _scrollableSubscription?.Dispose(); + _scrollableSubscription = null; + + if (scrollable != null) + { + scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); + _scrollableSubscription = new CompositeDisposable( + GetObservable(OffsetProperty).Skip(1).Subscribe(x => scrollable.Offset = x), + Disposable.Create(() => scrollable.InvalidateScroll = null)); + UpdateFromScrollable(scrollable); + } + } + + private void UpdateFromScrollable(IScrollable scrollable) + { + Viewport = scrollable.Viewport; + Extent = scrollable.Extent; + Offset = scrollable.Offset; + } + private static Vector ValidateOffset(ScrollContentPresenter o, Vector value) { return ScrollViewer.CoerceOffset( diff --git a/src/Perspex.Controls/Primitives/AdornerDecorator.cs b/src/Perspex.Controls/Primitives/AdornerDecorator.cs index 6fa8ec81b3..218e7b8360 100644 --- a/src/Perspex.Controls/Primitives/AdornerDecorator.cs +++ b/src/Perspex.Controls/Primitives/AdornerDecorator.cs @@ -10,7 +10,7 @@ namespace Perspex.Controls.Primitives AdornerLayer = new AdornerLayer(); ((ISetLogicalParent)AdornerLayer).SetParent(this); AdornerLayer.ZIndex = int.MaxValue; - AddVisualChild(AdornerLayer); + VisualChildren.Add(AdornerLayer); } public AdornerLayer AdornerLayer diff --git a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs index cf58646d4b..5f543cae17 100644 --- a/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Perspex.Controls/Primitives/HeaderedItemsControl.cs @@ -1,17 +1,58 @@ // 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 Perspex.Controls.Mixins; +using Perspex.Controls.Presenters; + namespace Perspex.Controls.Primitives { + /// + /// Represents an with a related header. + /// public class HeaderedItemsControl : ItemsControl { + /// + /// Defines the property. + /// public static readonly PerspexProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Initializes static members of the class. + /// + static HeaderedItemsControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } + + /// + /// Gets or sets the content of the control's header. + /// public object Header { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } } + + /// + /// Gets the header presenter from the control's template. + /// + public ContentPresenter HeaderPresenter + { + get; + private set; + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + } } } diff --git a/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs b/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs new file mode 100644 index 0000000000..9a6431c3e5 --- /dev/null +++ b/src/Perspex.Controls/Primitives/HeaderedSelectingControl.cs @@ -0,0 +1,58 @@ +// 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 Perspex.Controls.Mixins; +using Perspex.Controls.Presenters; + +namespace Perspex.Controls.Primitives +{ + /// + /// Represents a with a related header. + /// + public class HeaderedSelectingItemsControl : SelectingItemsControl + { + /// + /// Defines the property. + /// + public static readonly PerspexProperty HeaderProperty = + HeaderedContentControl.HeaderProperty.AddOwner(); + + /// + /// Initializes static members of the class. + /// + static HeaderedSelectingItemsControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } + + /// + /// Gets or sets the content of the control's header. + /// + public object Header + { + get { return GetValue(HeaderProperty); } + set { SetValue(HeaderProperty, value); } + } + + /// + /// Gets the header presenter from the control's template. + /// + public ContentPresenter HeaderPresenter + { + get; + private set; + } + + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + } + } +} diff --git a/src/Perspex.Controls/Primitives/IScrollInfo.cs b/src/Perspex.Controls/Primitives/IScrollInfo.cs deleted file mode 100644 index 59dbc085e6..0000000000 --- a/src/Perspex.Controls/Primitives/IScrollInfo.cs +++ /dev/null @@ -1,97 +0,0 @@ -// 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.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Perspex.Controls.Primitives -{ - public interface IScrollInfoBase - { - /// - /// ScrollOwner is the container that controls any scrollbars, headers, etc... that are dependant - /// on this IScrollInfo's properties. Implementers of IScrollInfo should call InvalidateScrollInfo() - /// on this object when properties change. - /// - ScrollViewer ScrollOwner { get; set; } - - Rect MakeVisible(Visual visual, Rect rectangle); - } - - public interface IVerticalScrollInfo : IScrollInfoBase - { - /// - /// VerticalOffset is the vertical offset into the scrolled content that represents the first unit visible. - /// - double VerticalOffset { get; set; } - - /// - /// ExtentHeight contains the full vertical range of the scrolled content. - /// - double ExtentHeight { get; } - - /// - /// ViewportHeight contains the currently visible vertical range of the scrolled content. - /// - double ViewportHeight { get; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the vertical given dimension. - /// - bool CanVerticallyScroll { get; set; } - - void LineDown(); - - void LineUp(); - - void MouseWheelDown(); - - void MouseWheelUp(); - - void PageDown(); - - void PageUp(); - } - - public interface IHorizontalScrollInfo : IScrollInfoBase - { - /// - /// ExtentWidth contains the full horizontal range of the scrolled content. - /// - double ExtentWidth { get; } - - /// - /// ViewportWidth contains the currently visible horizontal range of the scrolled content. - /// - double ViewportWidth { get; } - - /// - /// HorizontalOffset is the horizontal offset into the scrolled content that represents the first unit visible. - /// - double HorizontalOffset { get; set; } - - /// - /// This property indicates to the IScrollInfo whether or not it can scroll in the horizontal given dimension. - /// - bool CanHorizontallyScroll { get; set; } - - void LineLeft(); - - void LineRight(); - - void MouseWheelLeft(); - - void MouseWheelRight(); - - void PageLeft(); - - void PageRight(); - } - - public interface IScrollInfo : IHorizontalScrollInfo, IVerticalScrollInfo - { - } -} diff --git a/src/Perspex.Controls/Primitives/IScrollable.cs b/src/Perspex.Controls/Primitives/IScrollable.cs new file mode 100644 index 0000000000..752c02685d --- /dev/null +++ b/src/Perspex.Controls/Primitives/IScrollable.cs @@ -0,0 +1,44 @@ +// 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.Controls.Primitives +{ + /// + /// Interface implemented by controls that handle their own scrolling when placed inside a + /// . + /// + public interface IScrollable + { + /// + /// Gets or sets the scroll invalidation method. + /// + /// + /// + /// This method notifies the attached of a change in + /// the , or properties. + /// + /// + /// This property is set by the parent when the + /// is placed inside it. + /// + /// + Action InvalidateScroll { get; set; } + + /// + /// Gets the extent of the scrollable content, in logical units + /// + Size Extent { get; } + + /// + /// Gets or sets the current scroll offset, in logical units. + /// + Vector Offset { get; set; } + + /// + /// Gets the size of the viewport, in logical units. + /// + Size Viewport { get; } + } +} diff --git a/src/Perspex.Controls/Primitives/PopupRoot.cs b/src/Perspex.Controls/Primitives/PopupRoot.cs index 9abf981642..0847780bd8 100644 --- a/src/Perspex.Controls/Primitives/PopupRoot.cs +++ b/src/Perspex.Controls/Primitives/PopupRoot.cs @@ -44,7 +44,6 @@ namespace Perspex.Controls.Primitives public PopupRoot(IPerspexDependencyResolver dependencyResolver) : base(PlatformManager.CreatePopup(), dependencyResolver) { - GetObservable(ParentProperty).Subscribe(x => InheritanceParent = (PerspexObject)x); } /// @@ -85,9 +84,9 @@ namespace Perspex.Controls.Primitives } /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - base.OnTemplateApplied(nameScope); + base.OnTemplateApplied(e); if (Parent.TemplatedParent != null) { diff --git a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs b/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs deleted file mode 100644 index 8ea94f6fb6..0000000000 --- a/src/Perspex.Controls/Primitives/ScrollInfoAdapter.cs +++ /dev/null @@ -1,126 +0,0 @@ -// 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. - -namespace Perspex.Controls.Primitives -{ - public class ScrollInfoAdapter : IScrollInfo - { - private readonly IScrollInfoBase _nfo; - public ScrollInfoAdapter(IScrollInfoBase nfo) - { - _nfo = nfo; - } - - public ScrollViewer ScrollOwner - { - get { return _nfo.ScrollOwner; } - set { _nfo.ScrollOwner = value; } - } - - public double ExtentWidth => (_nfo as IHorizontalScrollInfo)?.ExtentWidth ?? 0; - - public double ViewportWidth => (_nfo as IHorizontalScrollInfo)?.ViewportWidth ?? 0; - - public double ExtentHeight => (_nfo as IVerticalScrollInfo)?.ExtentHeight ?? 0; - - public double ViewportHeight => (_nfo as IVerticalScrollInfo)?.ViewportHeight ?? 0; - - private double _horizontalOffset; - public double HorizontalOffset - { - get - { - return (_nfo as IHorizontalScrollInfo)?.HorizontalOffset ?? _horizontalOffset; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _horizontalOffset = value; - else - info.HorizontalOffset = value; - } - } - - private double _verticalOffset; - public double VerticalOffset - { - get - { - return (_nfo as IVerticalScrollInfo)?.VerticalOffset ?? _verticalOffset; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _verticalOffset = value; - else - info.VerticalOffset = value; - } - } - - public void LineLeft() => (_nfo as IHorizontalScrollInfo)?.LineLeft(); - - public void LineRight() => (_nfo as IHorizontalScrollInfo)?.LineRight(); - - public void MouseWheelLeft() => (_nfo as IHorizontalScrollInfo)?.MouseWheelLeft(); - - public void MouseWheelRight() => (_nfo as IHorizontalScrollInfo)?.MouseWheelRight(); - - public void PageLeft() => (_nfo as IHorizontalScrollInfo)?.PageLeft(); - - public Rect MakeVisible(Visual visual, Rect rectangle) => _nfo.MakeVisible(visual, rectangle); - - public void PageRight() => (_nfo as IHorizontalScrollInfo)?.PageRight(); - - public void LineDown() => (_nfo as IVerticalScrollInfo)?.LineDown(); - - public void LineUp() => (_nfo as IVerticalScrollInfo)?.LineUp(); - - public void MouseWheelDown() => (_nfo as IVerticalScrollInfo)?.MouseWheelDown(); - - public void MouseWheelUp() => (_nfo as IVerticalScrollInfo)?.MouseWheelUp(); - - public void PageDown() => (_nfo as IVerticalScrollInfo)?.PageDown(); - - public void PageUp() => (_nfo as IVerticalScrollInfo)?.PageUp(); - - private bool _canVerticallyScroll; - public bool CanVerticallyScroll - { - get - { - return (_nfo as IVerticalScrollInfo)?.CanVerticallyScroll ?? _canVerticallyScroll; - } - - set - { - var info = (_nfo as IVerticalScrollInfo); - if (info == null) - _canVerticallyScroll = value; - else - info.CanVerticallyScroll = value; - } - } - - private bool _canHorizontallyScroll; - public bool CanHorizontallyScroll - { - get - { - return (_nfo as IHorizontalScrollInfo)?.CanHorizontallyScroll ?? _canHorizontallyScroll; - } - - set - { - var info = (_nfo as IHorizontalScrollInfo); - if (info == null) - _canHorizontallyScroll = value; - else - info.CanHorizontallyScroll = value; - } - } - } -} diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index b60af28fda..20181a90fc 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -98,7 +98,6 @@ namespace Perspex.Controls.Primitives /// public SelectingItemsControl() { - ItemContainerGenerator.ContainersInitialized.Subscribe(ContainersInitialized); } /// @@ -286,6 +285,27 @@ namespace Perspex.Controls.Primitives } } + /// + protected override void OnContainersMaterialized(ItemContainerEventArgs e) + { + base.OnContainersMaterialized(e); + + var selectedIndex = SelectedIndex; + var selectedContainer = e.Containers + .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + + if (selectedContainer != null) + { + SelectedIndex = selectedContainer.Index; + } + else if (selectedIndex >= e.StartingIndex && + selectedIndex < e.StartingIndex + e.Containers.Count) + { + var container = e.Containers[selectedIndex - e.StartingIndex]; + MarkContainerSelected(container.ContainerControl, true); + } + } + /// protected override void OnDataContextChanged() { @@ -379,7 +399,7 @@ namespace Perspex.Controls.Primitives bool rangeModifier = false, bool toggleModifier = false) { - var index = ItemContainerGenerator.IndexFromContainer(container); + var index = ItemContainerGenerator?.IndexFromContainer(container) ?? -1; if (index != -1) { @@ -416,61 +436,6 @@ namespace Perspex.Controls.Primitives return false; } - /// - /// Gets the item at the specified index in a collection. - /// - /// The collection. - /// The index. - /// The index of the item or -1 if the item was not found. - private static object ElementAt(IEnumerable items, int index) - { - var typedItems = items?.Cast(); - - if (index != -1 && typedItems != null && index < typedItems.Count()) - { - return typedItems.ElementAt(index) ?? null; - } - else - { - return null; - } - } - - /// - /// Gets the index of an item in a collection. - /// - /// The collection. - /// The item. - /// The index of the item or -1 if the item was not found. - private static int IndexOf(IEnumerable items, object item) - { - if (items != null && item != null) - { - var list = items as IList; - - if (list != null) - { - return list.IndexOf(item); - } - else - { - int index = 0; - - foreach (var i in items) - { - if (Equals(i, item)) - { - return index; - } - - ++index; - } - } - } - - return -1; - } - /// /// Gets a range of items from an IEnumerable. /// @@ -523,27 +488,6 @@ namespace Perspex.Controls.Primitives } } - /// - /// Called when new containers are initialized by the . - /// - /// The containers. - private void ContainersInitialized(ItemContainers containers) - { - var selectedIndex = SelectedIndex; - var selectedContainer = containers.Items.OfType().FirstOrDefault(x => x.IsSelected); - - if (selectedContainer != null) - { - SelectedIndex = containers.Items.IndexOf((IControl)selectedContainer) + containers.StartingIndex; - } - else if (selectedIndex >= containers.StartingIndex && - selectedIndex < containers.StartingIndex + containers.Items.Count) - { - var container = containers.Items[selectedIndex - containers.StartingIndex]; - MarkContainerSelected(container, true); - } - } - /// /// Called when a container raises the . /// @@ -593,7 +537,6 @@ namespace Perspex.Controls.Primitives try { var selectable = container as ISelectable; - var styleable = container as IStyleable; _ignoreContainerSelectionChanged = true; @@ -601,16 +544,9 @@ namespace Perspex.Controls.Primitives { selectable.IsSelected = selected; } - else if (styleable != null) + else { - if (selected) - { - styleable.Classes.Add(":selected"); - } - else - { - styleable.Classes.Remove(":selected"); - } + ((IPseudoClasses)container.Classes).Set(":selected", selected); } } finally @@ -626,7 +562,7 @@ namespace Perspex.Controls.Primitives /// Whether the item should be selected or deselected. private void MarkItemSelected(int index, bool selected) { - var container = ItemContainerGenerator.ContainerFromIndex(index); + var container = ItemContainerGenerator?.ContainerFromIndex(index); if (container != null) { @@ -656,6 +592,8 @@ namespace Perspex.Controls.Primitives /// The event args. private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { + var generator = ItemContainerGenerator; + switch (e.Action) { case NotifyCollectionChangedAction.Add: @@ -681,9 +619,15 @@ namespace Perspex.Controls.Primitives break; case NotifyCollectionChangedAction.Reset: - foreach (var item in ItemContainerGenerator.Containers) + if (generator != null) { - MarkContainerSelected(item, false); + foreach (var item in generator.Containers) + { + if (item != null) + { + MarkContainerSelected(item.ContainerControl, false); + } + } } if (!_syncingSelectedItems) diff --git a/src/Perspex.Controls/Primitives/TabStrip.cs b/src/Perspex.Controls/Primitives/TabStrip.cs index 5fc305c73a..1cfb4e0515 100644 --- a/src/Perspex.Controls/Primitives/TabStrip.cs +++ b/src/Perspex.Controls/Primitives/TabStrip.cs @@ -1,52 +1,26 @@ // 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 System.Reactive.Linq; using Perspex.Controls.Generators; +using Perspex.Controls.Templates; using Perspex.Input; namespace Perspex.Controls.Primitives { public class TabStrip : SelectingItemsControl { - public static readonly PerspexProperty SelectedTabProperty = - TabControl.SelectedTabProperty.AddOwner(); + private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); static TabStrip() { + MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); } - public TabStrip() - { - GetObservable(SelectedItemProperty).Subscribe(x => SelectedTab = x as TabItem); - GetObservable(SelectedTabProperty).Subscribe(x => SelectedItem = x as TabItem); - } - - public TabItem SelectedTab - { - get { return GetValue(SelectedTabProperty); } - set { SetValue(SelectedTabProperty, value); } - } - protected override IItemContainerGenerator CreateItemContainerGenerator() { - TabControl tabControl = TemplatedParent as TabControl; - IItemContainerGenerator result; - - if (tabControl != null) - { - result = tabControl.ItemContainerGenerator; - } - else - { - result = new ItemContainerGenerator(this, TabItem.ContentProperty); - } - - return result; + return new ItemContainerGenerator(this, ContentControl.ContentProperty); } /// @@ -70,5 +44,11 @@ namespace Perspex.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } + + private static object SelectHeader(object o) + { + var headered = o as IHeadered; + return (headered != null) ? (headered.Header ?? string.Empty) : o; + } } } diff --git a/src/Perspex.Controls/Primitives/TabStripItem.cs b/src/Perspex.Controls/Primitives/TabStripItem.cs new file mode 100644 index 0000000000..5e6787c167 --- /dev/null +++ b/src/Perspex.Controls/Primitives/TabStripItem.cs @@ -0,0 +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. + +namespace Perspex.Controls.Primitives +{ + /// + /// Represents a tab in a . + /// + public class TabStripItem : ListBoxItem + { + } +} diff --git a/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs b/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.cs new file mode 100644 index 0000000000..e921f40785 --- /dev/null +++ b/src/Perspex.Controls/Primitives/TemplateAppliedEventArgs.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 Perspex.Interactivity; + +namespace Perspex.Controls.Primitives +{ + /// + /// Holds the details of the event. + /// + public class TemplateAppliedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The applied template's name scope. + public TemplateAppliedEventArgs(INameScope nameScope) + : base(TemplatedControl.TemplateAppliedEvent) + { + NameScope = nameScope; + } + + /// + /// Gets the name scope of the applied template. + /// + public INameScope NameScope { get; } + } +} diff --git a/src/Perspex.Controls/Primitives/TemplatedControl.cs b/src/Perspex.Controls/Primitives/TemplatedControl.cs index 9e98ad154c..09b90a2e1e 100644 --- a/src/Perspex.Controls/Primitives/TemplatedControl.cs +++ b/src/Perspex.Controls/Primitives/TemplatedControl.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Interactivity; using Perspex.Media; using Perspex.Styling; using Perspex.VisualTree; @@ -72,6 +74,14 @@ namespace Perspex.Controls.Primitives public static readonly PerspexProperty TemplateProperty = PerspexProperty.Register("Template"); + /// + /// Defines the routed event. + /// + public static readonly RoutedEvent TemplateAppliedEvent = + RoutedEvent.Register( + "TemplateApplied", + RoutingStrategies.Direct); + private bool _templateApplied; private readonly ILogger _templateLog; @@ -81,12 +91,8 @@ namespace Perspex.Controls.Primitives /// static TemplatedControl() { - TemplateProperty.Changed.Subscribe(e => - { - var templatedControl = (TemplatedControl)e.Sender; - templatedControl._templateApplied = false; - templatedControl.InvalidateMeasure(); - }); + ClipToBoundsProperty.OverrideDefaultValue(true); + TemplateProperty.Changed.AddClassHandler(x => x.OnTemplateChanged); } /// @@ -102,6 +108,15 @@ namespace Perspex.Controls.Primitives }); } + /// + /// Raised when the control's template is applied. + /// + public event EventHandler TemplateApplied + { + add { AddHandler(TemplateAppliedEvent, value); } + remove { RemoveHandler(TemplateAppliedEvent, value); } + } + /// /// Gets or sets the brush used to draw the control's background. /// @@ -188,7 +203,7 @@ namespace Perspex.Controls.Primitives { if (!_templateApplied) { - ClearVisualChildren(); + VisualChildren.Clear(); if (Template != null) { @@ -198,30 +213,60 @@ namespace Perspex.Controls.Primitives var nameScope = new NameScope(); NameScope.SetNameScope((Control)child, nameScope); - // We need to call SetTemplatedParentAndApplyChildTemplates twice - once - // before the controls are added to the visual tree so that the logical - // tree can be set up before styling is applied. - ((ISetLogicalParent)child).SetParent(this); + // We need to call SetupTemplateControls twice: + // - Once before the controls are added to the visual/logical trees so that the + // TemplatedParent property is set and names are registered; if + // TemplatedParent is not set when the control is added to the logical tree, + // then styles with the /template/ selector won't match. + // - Once after the controls are added to the logical tree (and thus styled) to + // call ApplyTemplate on nested templated controls and register any of our + // templated children that appear as children of presenters in these nested + // templated child controls. SetupTemplateControls(child, nameScope); - - // And again after the controls are added to the visual tree, and have their - // styling and thus Template property set. - AddVisualChild((Visual)child); + VisualChildren.Add(child); + ((ISetLogicalParent)child).SetParent(this); SetupTemplateControls(child, nameScope); - OnTemplateApplied(nameScope); + OnTemplateApplied(new TemplateAppliedEventArgs(nameScope)); } _templateApplied = true; } } + protected sealed override BindingDescriptor CreateBindingDescriptor(BindingDescriptor source) + { + var result = base.CreateBindingDescriptor(source); + + // If the binding is a template binding, then complete when the Template changes. + if (source.Priority == BindingPriority.TemplatedParent) + { + var templateChanged = GetObservable(TemplateProperty).Skip(1); + + result.SourceObservable = result.Source.GetObservable(result.Property) + .TakeUntil(templateChanged); + } + + return result; + } + /// /// Called when the control's template is applied. /// - /// The template name scope. - protected virtual void OnTemplateApplied(INameScope nameScope) + /// The event args. + protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e) + { + RaiseEvent(e); + } + + /// + /// Called when the property changes. + /// + /// The event args. + protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e) { + _templateApplied = false; + InvalidateMeasure(); } /// diff --git a/src/Perspex.Controls/Primitives/Track.cs b/src/Perspex.Controls/Primitives/Track.cs index ae9a8d1c4a..4e343b65b3 100644 --- a/src/Perspex.Controls/Primitives/Track.cs +++ b/src/Perspex.Controls/Primitives/Track.cs @@ -45,12 +45,12 @@ namespace Perspex.Controls.Primitives val.Item1.DragDelta -= ThumbDragged; } - ClearVisualChildren(); + VisualChildren.Clear(); if (val.Item2 != null) { val.Item2.DragDelta += ThumbDragged; - AddVisualChild(val.Item2); + VisualChildren.Add(val.Item2); } }); } diff --git a/src/Perspex.Controls/ProgressBar.cs b/src/Perspex.Controls/ProgressBar.cs index 4bc1fdb9d2..0cb512d50e 100644 --- a/src/Perspex.Controls/ProgressBar.cs +++ b/src/Perspex.Controls/ProgressBar.cs @@ -26,9 +26,9 @@ namespace Perspex.Controls } /// - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - _indicator = nameScope.Get("PART_Indicator"); + _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); } diff --git a/src/Perspex.Controls/ScrollViewer.cs b/src/Perspex.Controls/ScrollViewer.cs index 5af724a614..b454bc6d5e 100644 --- a/src/Perspex.Controls/ScrollViewer.cs +++ b/src/Perspex.Controls/ScrollViewer.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; @@ -117,6 +120,8 @@ namespace Perspex.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + private IDisposable _scrollableSubscription; + /// /// Initializes static members of the class. /// diff --git a/src/Perspex.Controls/TabControl.cs b/src/Perspex.Controls/TabControl.cs index bff505497f..86ca837a4e 100644 --- a/src/Perspex.Controls/TabControl.cs +++ b/src/Perspex.Controls/TabControl.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Perspex.Animation; -using Perspex.Controls.Presenters; +using Perspex.Controls.Generators; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; @@ -11,23 +11,26 @@ namespace Perspex.Controls /// /// A tab control that displays a tab strip along with the content of the selected tab. /// - public class TabControl : SelectingItemsControl, IReparentingHost + public class TabControl : SelectingItemsControl { - /// - /// Defines the property. - /// - public static readonly PerspexProperty SelectedTabProperty = - PerspexProperty.Register("SelectedTab"); - /// /// Defines the property. /// public static readonly PerspexProperty TransitionProperty = - Carousel.TransitionProperty.AddOwner(); + Perspex.Controls.Carousel.TransitionProperty.AddOwner(); - private static readonly IMemberSelector s_contentSelector = + /// + /// Defines an that selects the content of a . + /// + public static readonly IMemberSelector ContentSelector = new FuncMemberSelector(SelectContent); + /// + /// Defines an that selects the header of a . + /// + public static readonly IMemberSelector HeaderSelector = + new FuncMemberSelector(SelectHeader); + /// /// Defines the property. /// @@ -41,22 +44,25 @@ namespace Perspex.Controls { SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(false); - SelectedItemProperty.Changed.AddClassHandler(x => x.SelectedItemChanged); AffectsMeasure(TabStripPlacementProperty); } /// - /// Gets an that selects the content of a . + /// Gets the pages portion of the 's template. /// - public IMemberSelector ContentSelector => s_contentSelector; + public IControl Pages + { + get; + private set; + } /// - /// Gets the as a . + /// Gets the tab strip portion of the 's template. /// - public TabItem SelectedTab + public IControl TabStrip { - get { return GetValue(SelectedTabProperty); } - private set { SetValue(SelectedTabProperty, value); } + get; + private set; } /// @@ -77,17 +83,21 @@ namespace Perspex.Controls set { SetValue(TabStripPlacementProperty, value); } } - /// - /// Asks the control whether it wants to reparent the logical children of the specified - /// control. - /// - /// The control. - /// - /// True if the control wants to reparent its logical children otherwise false. - /// - bool IReparentingHost.WillReparentChildrenOf(IControl control) + protected override IItemContainerGenerator CreateItemContainerGenerator() { - return control is CarouselPresenter; + // TabControl doesn't actually create items - instead its TabStrip and Carousel + // children create the items. However we want it to be a SelectingItemsControl + // so that it has the Items/SelectedItem etc properties. In this case, we can + // return a null ItemContainerGenerator to disable the creation of item containers. + return null; + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + + TabStrip = e.NameScope.Find("PART_TabStrip"); + Pages = e.NameScope.Find("PART_Content"); } /// @@ -110,14 +120,31 @@ namespace Perspex.Controls } /// - /// Called when the property changes. + /// Selects the header of a tab item. /// - /// The event args. - private void SelectedItemChanged(PerspexPropertyChangedEventArgs e) + /// The tab item. + /// The content. + private static object SelectHeader(object o) { - var item = e.NewValue as IContentControl; - var content = item?.Content ?? item; - SelectedTab = item as TabItem; + var headered = o as IHeadered; + var control = o as IControl; + + if (headered != null) + { + return headered.Header ?? string.Empty; + } + else if (control != null) + { + // Non-headered control items should result in TabStripItems with empty content. + // If a TabStrip is created with non IHeadered controls as its items, don't try to + // display the control in the TabStripItem: the content portion will also try to + // display this control, resulting in dual-parentage breakage. + return string.Empty; + } + else + { + return o; + } } } } diff --git a/src/Perspex.Controls/Templates/TemplateExtensions.cs b/src/Perspex.Controls/Templates/TemplateExtensions.cs index b8cd9e25c2..40672f873e 100644 --- a/src/Perspex.Controls/Templates/TemplateExtensions.cs +++ b/src/Perspex.Controls/Templates/TemplateExtensions.cs @@ -12,35 +12,6 @@ namespace Perspex.Controls.Templates { public static class TemplateExtensions { - public static IReparentingHost FindReparentingHost(this IControl control) - { - var tp = control.TemplatedParent; - var chain = new List(); - - while (tp != null) - { - var reparentingHost = tp as IReparentingHost; - var styleable = tp as IStyleable; - - if (reparentingHost != null) - { - chain.Add(reparentingHost); - } - - tp = styleable?.TemplatedParent ?? null; - } - - foreach (var reparenting in chain.AsEnumerable().Reverse()) - { - if (reparenting.WillReparentChildrenOf(control)) - { - return reparenting; - } - } - - return null; - } - public static IEnumerable GetTemplateChildren(this ITemplatedControl control) { var visual = control as IVisual; diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index 1442bae1f2..ea749be84e 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -26,6 +26,7 @@ namespace Perspex.Controls public static readonly PerspexProperty FontFamilyProperty = PerspexProperty.RegisterAttached( nameof(FontFamily), + defaultValue: "Courier New", inherits: true); /// @@ -34,6 +35,7 @@ namespace Perspex.Controls public static readonly PerspexProperty FontSizeProperty = PerspexProperty.RegisterAttached( nameof(FontSize), + defaultValue: 12, inherits: true); /// @@ -343,8 +345,8 @@ namespace Perspex.Controls { var result = new FormattedText( Text ?? string.Empty, - FontFamily ?? "Arial", - FontSize > 0 ? FontSize : 12, + FontFamily, + FontSize, FontStyle, TextAlignment, FontWeight); diff --git a/src/Perspex.Controls/TextBox.cs b/src/Perspex.Controls/TextBox.cs index 7eddecf0f7..0bfc8d2eeb 100644 --- a/src/Perspex.Controls/TextBox.cs +++ b/src/Perspex.Controls/TextBox.cs @@ -157,9 +157,9 @@ namespace Perspex.Controls set { SetValue(TextWrappingProperty, value); } } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { - _presenter = nameScope.Get("PART_TextPresenter"); + _presenter = e.NameScope.Get("PART_TextPresenter"); _presenter.Cursor = new Cursor(StandardCursorType.Ibeam); } diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index f167c62770..4dceab17be 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -24,7 +24,7 @@ namespace Perspex.Controls /// . It handles scheduling layout, styling and rendering as well as /// tracking the window and state. /// - public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable + public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot { /// /// Defines the property. @@ -204,6 +204,11 @@ namespace Perspex.Controls set { SetValue(AccessText.ShowAccessKeyProperty, value); } } + IStyleHost IStyleHost.StylingParent + { + get { return PerspexLocator.Current.GetService(); } + } + /// /// Whether an auto-size operation is in progress. /// diff --git a/src/Perspex.Controls/TreeView.cs b/src/Perspex.Controls/TreeView.cs index a1d14d5441..4b7b1f986a 100644 --- a/src/Perspex.Controls/TreeView.cs +++ b/src/Perspex.Controls/TreeView.cs @@ -58,7 +58,7 @@ namespace Perspex.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty, - null); + new TreeContainerIndex()); } /// @@ -101,13 +101,13 @@ namespace Perspex.Controls bool rangeModifier = false, bool toggleModifier = false) { - var item = ItemContainerGenerator.TreeItemFromContainer(container); + var item = ItemContainerGenerator.Index.ItemFromContainer(container); if (item != null) { if (SelectedItem != null) { - var old = ItemContainerGenerator.TreeContainerFromItem(SelectedItem); + var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem); MarkContainerSelected(old, false); } @@ -162,7 +162,7 @@ namespace Perspex.Controls if (item != null) { - if (item.ItemContainerGenerator.RootGenerator == this.ItemContainerGenerator) + if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index) { return item; } @@ -179,22 +179,14 @@ namespace Perspex.Controls private void MarkContainerSelected(IControl container, bool selected) { var selectable = container as ISelectable; - var styleable = container as IStyleable; if (selectable != null) { selectable.IsSelected = selected; } - else if (styleable != null) + else { - if (selected) - { - styleable.Classes.Add(":selected"); - } - else - { - styleable.Classes.Remove(":selected"); - } + ((IPseudoClasses)container.Classes).Set(":selected", selected); } } } diff --git a/src/Perspex.Controls/TreeViewItem.cs b/src/Perspex.Controls/TreeViewItem.cs index da263ca951..e2d8107cfc 100644 --- a/src/Perspex.Controls/TreeViewItem.cs +++ b/src/Perspex.Controls/TreeViewItem.cs @@ -7,8 +7,7 @@ using Perspex.Controls.Mixins; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; -using Perspex.Rendering; -using Perspex.VisualTree; +using Perspex.LogicalTree; namespace Perspex.Controls { @@ -79,14 +78,20 @@ namespace Perspex.Controls TreeViewItem.HeaderProperty, TreeViewItem.ItemsProperty, TreeViewItem.IsExpandedProperty, - _treeView?.ItemContainerGenerator); + _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex()); } /// - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { - base.OnAttachedToVisualTree(e); - _treeView = this.GetVisualAncestors().OfType().FirstOrDefault(); + base.OnAttachedToLogicalTree(e); + _treeView = this.GetLogicalAncestors().OfType().FirstOrDefault(); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnDetachedFromLogicalTree(e); + ItemContainerGenerator.Clear(); } /// diff --git a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs b/src/Perspex.Diagnostics/ViewModels/TreeNode.cs index 837930aadb..2ecb1939b7 100644 --- a/src/Perspex.Diagnostics/ViewModels/TreeNode.cs +++ b/src/Perspex.Diagnostics/ViewModels/TreeNode.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 System.Collections.Specialized; using System.Reactive; using System.Reactive.Linq; using Perspex.Controls; @@ -18,7 +19,13 @@ namespace Perspex.Diagnostics.ViewModels Control = control; Type = control.GetType().Name; - control.Classes.Changed.Select(_ => Unit.Default) + var classesChanged = Observable.FromEventPattern< + NotifyCollectionChangedEventHandler, + NotifyCollectionChangedEventArgs>( + x => control.Classes.CollectionChanged += x, + x => control.Classes.CollectionChanged -= x); + + classesChanged.Select(_ => Unit.Default) .StartWith(Unit.Default) .Subscribe(_ => { diff --git a/src/Perspex.Layout/LayoutManager.cs b/src/Perspex.Layout/LayoutManager.cs index f1c45693ab..326549ccae 100644 --- a/src/Perspex.Layout/LayoutManager.cs +++ b/src/Perspex.Layout/LayoutManager.cs @@ -226,12 +226,12 @@ namespace Perspex.Layout { var parent = item.Control.GetVisualParent(); - while (parent.PreviousMeasure == null) + while (parent != null && parent.PreviousMeasure == null) { parent = parent.GetVisualParent(); } - if (parent.GetVisualRoot() == Root) + if (parent != null && parent.GetVisualRoot() == Root) { parent.Measure(parent.PreviousMeasure.Value, true); } diff --git a/src/Perspex.Layout/Layoutable.cs b/src/Perspex.Layout/Layoutable.cs index 018cc6a16c..e09917b12b 100644 --- a/src/Perspex.Layout/Layoutable.cs +++ b/src/Perspex.Layout/Layoutable.cs @@ -472,7 +472,7 @@ namespace Perspex.Layout height = Math.Min(height, MaxHeight); height = Math.Max(height, MinHeight); - return new Size(width, height).Inflate(Margin); + return NonNegative(new Size(width, height).Inflate(Margin)); } else { @@ -649,6 +649,16 @@ namespace Perspex.Layout double.IsNaN(size.Width) || double.IsNaN(size.Height); } + /// + /// Ensures neither component of a is negative. + /// + /// The size. + /// The non-negative size. + private static Size NonNegative(Size size) + { + return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0)); + } + /// /// Gets the layout root, together with its distance. /// diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index a6c4f6da14..a7be9919f3 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -118,7 +118,7 @@ - + diff --git a/src/Perspex.SceneGraph/Point.cs b/src/Perspex.SceneGraph/Point.cs index d87c570e9c..6a618dd893 100644 --- a/src/Perspex.SceneGraph/Point.cs +++ b/src/Perspex.SceneGraph/Point.cs @@ -229,6 +229,24 @@ namespace Perspex return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); } + /// + /// Transforms the point by a matrix. + /// + /// The transform. + /// The transformed point. + public Point Transform(Matrix transform) + { + var x = X; + var y = Y; + var xadd = y * transform.M21 + transform.M31; + var yadd = x * transform.M12 + transform.M32; + x *= transform.M11; + x += xadd; + y *= transform.M22; + y += yadd; + return new Point(x, y); + } + /// /// Returns a new point with the specified X coordinate. /// diff --git a/src/Perspex.SceneGraph/Rect.cs b/src/Perspex.SceneGraph/Rect.cs index c3d3a95111..af8a32a593 100644 --- a/src/Perspex.SceneGraph/Rect.cs +++ b/src/Perspex.SceneGraph/Rect.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Linq; namespace Perspex { @@ -11,6 +12,11 @@ namespace Perspex /// public struct Rect { + /// + /// An empty rectangle. + /// + public static readonly Rect Empty = default(Rect); + /// /// The X position. /// @@ -328,25 +334,74 @@ namespace Perspex /// The intersection. public Rect Intersect(Rect rect) { - double x = Math.Max(_x, rect._x); - double y = Math.Max(_y, rect._y); - double width = Math.Min(Right, rect.Right) - x; - double height = Math.Min(Bottom, rect.Bottom) - y; + var newLeft = (rect.X > X) ? rect.X : X; + var newTop = (rect.Y > Y) ? rect.Y : Y; + var newRight = (rect.Right < Right) ? rect.Right : Right; + var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom; - if (width < 0 || height < 0) + if ((newRight > newLeft) && (newBottom > newTop)) { - return new Rect( - double.PositiveInfinity, - double.PositiveInfinity, - double.NegativeInfinity, - double.NegativeInfinity); + return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop); } else { - return new Rect(x, y, width, height); + return Empty; } } + /// + /// Determines whether a rectangle intersects with this rectangle. + /// + /// The other rectangle. + /// + /// True if the specified rectangle intersects with this one; otherwise false. + /// + public bool Intersects(Rect rect) + { + return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); + } + + /// + /// Returns the axis-aligned bounding box of a transformed rectangle. + /// + /// The transform. + /// The bounding box + public Rect TransformToAABB(Matrix matrix) + { + var points = new[] + { + TopLeft.Transform(matrix), + TopRight.Transform(matrix), + BottomRight.Transform(matrix), + BottomLeft.Transform(matrix), + }; + + var left = double.MaxValue; + var right = double.MinValue; + var top = double.MaxValue; + var bottom = double.MinValue; + + foreach (var p in points) + { + if (p.X < left) left = p.X; + if (p.X > right) right = p.X; + if (p.Y < top) top = p.Y; + if (p.Y > bottom) bottom = p.Y; + } + + return new Rect(new Point(left, top), new Point(right, bottom)); + } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public Rect Translate(Vector offset) + { + return new Rect(Position + offset, Size); + } + /// /// Returns the string representation of the rectangle. /// diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs similarity index 59% rename from src/Perspex.SceneGraph/Rendering/RendererBase.cs rename to src/Perspex.SceneGraph/Rendering/RendererMixin.cs index 24dbab3622..a820eaf65e 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Perspex.Media; using Perspex.Platform; namespace Perspex.Rendering { /// - /// Base class for standard renderers. + /// Extension methods for rendering. /// /// /// This class provides implements the platform-independent parts of . @@ -21,11 +20,20 @@ namespace Perspex.Rendering [SuppressMessage("ReSharper", "ForCanBeConvertedToForeach")] public static class RendererMixin { - static int s_frameNum; - static int s_fps; - static int s_currentFrames; - static TimeSpan s_lastMeasure; - static readonly Stopwatch s_stopwatch = Stopwatch.StartNew(); + private static int s_frameNum; + private static int s_fps; + private static int s_currentFrames; + private static TimeSpan s_lastMeasure; + private static readonly Stopwatch s_stopwatch = Stopwatch.StartNew(); + private static readonly Stack> s_listPool = new Stack>(); + private static readonly ZIndexComparer s_visualComparer = new ZIndexComparer(); + + /// + /// Gets or sets a value which determines whether an FPS counted will be drawn on each + /// rendered frame. + /// + public static bool DrawFpsCounter { get; set; } + /// /// Renders the specified visual. /// @@ -62,17 +70,30 @@ namespace Perspex.Rendering } } - public static bool DrawFpsCounter { get; set; } - /// /// Renders the specified visual. /// /// The visual to render. - /// /// The drawing context. public static void Render(this DrawingContext context, IVisual visual) + { + context.Render(visual, visual.Bounds); + } + + /// + /// Renders the specified visual. + /// + /// The visual to render. + /// The drawing context. + /// + /// The current clip rect, in coordinates relative to . + /// + private static void Render(this DrawingContext context, IVisual visual, Rect clipRect) { var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; + var bounds = new Rect(visual.Bounds.Size); + if (visual.IsVisible && opacity > 0) { var m = Matrix.CreateTranslation(visual.Bounds.Position); @@ -83,49 +104,71 @@ namespace Perspex.Rendering { var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset)*visual.RenderTransform.Value*(offset); + renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + } + + m = renderTransform * m; + + if (clipToBounds) + { + clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); } - m = renderTransform*m; using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) + using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) { visual.Render(context); + var lst = GetSortedVisualList(visual.VisualChildren); + foreach (var child in lst) { - context.Render(child); + var childBounds = GetTransformedBounds(child); + + if (!child.ClipToBounds || clipRect.Intersects(childBounds)) + { + var childClipRect = clipRect.Translate(-childBounds.Position); + context.Render(child, childClipRect); + } } + ReturnListToPool(lst); } } } - static readonly Stack> ListPool = new Stack>(); - static readonly ZIndexComparer VisualComparer = new ZIndexComparer(); - class ZIndexComparer : IComparer - { - public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex); - } - - static void ReturnListToPool(List lst) + private static void ReturnListToPool(List lst) { lst.Clear(); - ListPool.Push(lst); + s_listPool.Push(lst); } - static List GetSortedVisualList(IReadOnlyList source) + private static List GetSortedVisualList(IReadOnlyList source) { - var lst = ListPool.Count == 0 ? new List() : ListPool.Pop(); + var lst = s_listPool.Count == 0 ? new List() : s_listPool.Pop(); for (var c = 0; c < source.Count; c++) lst.Add(source[c]); - lst.Sort(VisualComparer); + lst.Sort(s_visualComparer); return lst; } + private static Rect GetTransformedBounds(IVisual visual) + { + if (visual.RenderTransform == null) + { + return visual.Bounds; + } + else + { + return visual.Bounds.TransformToAABB(visual.RenderTransform.Value); + } + } - + class ZIndexComparer : IComparer + { + public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex); + } } } diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index 597528d0a7..e8fa579960 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -81,11 +81,6 @@ namespace Perspex /// private string _name; - /// - /// Holds the children of the visual. - /// - private readonly PerspexList _visualChildren; - /// /// The visual's bounds relative to its parent. /// @@ -128,9 +123,10 @@ namespace Perspex new PropertyEnricher("Id", GetHashCode()), }); - _visualChildren = new PerspexList(); - _visualChildren.ResetBehavior = ResetBehavior.Remove; - _visualChildren.CollectionChanged += VisualChildrenChanged; + var visualChildren = new PerspexList(); + visualChildren.ResetBehavior = ResetBehavior.Remove; + visualChildren.CollectionChanged += VisualChildrenChanged; + VisualChildren = visualChildren; } /// @@ -249,6 +245,15 @@ namespace Perspex set { SetValue(ZIndexProperty, value); } } + /// + /// Gets the control's visual children. + /// + protected IPerspexList VisualChildren + { + get; + private set; + } + /// /// Gets a value indicating whether this scene graph node is attached to a visual root. /// @@ -257,7 +262,7 @@ namespace Perspex /// /// Gets the scene graph node's child nodes. /// - IPerspexReadOnlyList IVisual.VisualChildren => _visualChildren; + IPerspexReadOnlyList IVisual.VisualChildren => VisualChildren; /// /// Gets the scene graph node's parent node. @@ -333,61 +338,6 @@ namespace Perspex property.Changed.Subscribe(AffectsRenderInvalidate); } - /// - /// Adds a visual child to the control. - /// - /// The child to add. - protected void AddVisualChild(IVisual visual) - { - Contract.Requires(visual != null); - - _visualChildren.Add(visual); - } - - /// - /// Adds visual children to the control. - /// - /// The children to add. - protected void AddVisualChildren(IEnumerable visuals) - { - Contract.Requires(visuals != null); - - _visualChildren.AddRange(visuals); - } - - /// - /// Removes all visual children from the control. - /// - protected void ClearVisualChildren() - { - _visualChildren.Clear(); - } - - /// - /// Removes a visual child from the control; - /// - /// The child to remove. - protected void RemoveVisualChild(IVisual visual) - { - Contract.Requires(visual != null); - - _visualChildren.Remove(visual); - } - - /// - /// Removes a visual children from the control; - /// - /// The children to remove. - protected void RemoveVisualChildren(IEnumerable visuals) - { - Contract.Requires(visuals != null); - - foreach (var v in visuals) - { - _visualChildren.Remove(v); - } - } - /// /// Called when the control is added to a visual tree. /// @@ -433,20 +383,6 @@ namespace Perspex (e.Sender as Visual)?.InvalidateVisual(); } - /// - /// Gets the event args for an or - /// event. - /// - /// - /// A if the visual currently has a root; - /// otherwise null. - /// - private VisualTreeAttachmentEventArgs GetAttachmentEventArgs() - { - var root = this.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - return root != null ? new VisualTreeAttachmentEventArgs(root) : null; - } - /// /// Gets the visual offset from the specified ancestor. /// @@ -544,38 +480,28 @@ namespace Perspex } var old = _visualParent; + _visualParent = value; if (_isAttachedToVisualTree) { - var oldArgs = GetAttachmentEventArgs(); - - _visualParent = value; - - if (oldArgs != null) - { - NotifyDetachedFromVisualTree(oldArgs); - } - } - else - { - _visualParent = value; + var root = (this as IRenderRoot) ?? + old.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); + var e = new VisualTreeAttachmentEventArgs(root); + NotifyDetachedFromVisualTree(e); } if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true) { - var newArgs = GetAttachmentEventArgs(); - - if (newArgs != null) - { - NotifyAttachedToVisualTree(newArgs); - } + var root = this.GetVisualAncestors().OfType().FirstOrDefault(); + var e = new VisualTreeAttachmentEventArgs(root); + NotifyAttachedToVisualTree(e); } RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue); } /// - /// Called when the collection changes. + /// Called when the collection changes. /// /// The sender. /// The event args. @@ -586,7 +512,6 @@ namespace Perspex case NotifyCollectionChangedAction.Add: foreach (Visual v in e.NewItems) { - v.InheritanceParent = this; v.SetVisualParent(this); } @@ -595,7 +520,6 @@ namespace Perspex case NotifyCollectionChangedAction.Remove: foreach (Visual v in e.OldItems) { - v.InheritanceParent = null; v.SetVisualParent(null); } @@ -616,9 +540,9 @@ namespace Perspex OnAttachedToVisualTree(e); - if (_visualChildren != null) + if (VisualChildren != null) { - foreach (Visual child in _visualChildren.OfType()) + foreach (Visual child in VisualChildren.OfType()) { child.NotifyAttachedToVisualTree(e); } @@ -637,9 +561,9 @@ namespace Perspex _isAttachedToVisualTree = false; OnDetachedFromVisualTree(e); - if (_visualChildren != null) + if (VisualChildren != null) { - foreach (Visual child in _visualChildren.OfType()) + foreach (Visual child in VisualChildren.OfType()) { child.NotifyDetachedFromVisualTree(e); } diff --git a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs index d0bc9d763a..9c243efc0c 100644 --- a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs +++ b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs @@ -47,7 +47,7 @@ namespace Perspex.VisualTree var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds); // TODO: Track transform and clip rectangle. - return bounds.Select(x => new TransformedBounds((Rect)x, (Rect)new Rect(), (Matrix)Matrix.Identity)); + return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity)); } /// diff --git a/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs index b2df5a9a34..a6f1081ed3 100644 --- a/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs +++ b/src/Perspex.SceneGraph/VisualTreeAttachmentEventArgs.cs @@ -18,6 +18,8 @@ namespace Perspex /// The root visual. public VisualTreeAttachmentEventArgs(IRenderRoot root) { + Contract.Requires(root != null); + Root = root; } diff --git a/src/Perspex.Styling/ILogical.cs b/src/Perspex.Styling/ILogical.cs index a62e309021..b1f5ded5b6 100644 --- a/src/Perspex.Styling/ILogical.cs +++ b/src/Perspex.Styling/ILogical.cs @@ -10,6 +10,11 @@ namespace Perspex /// public interface ILogical { + /// + /// Gets a value indicating whether the element is attached to a rooted logical tree. + /// + bool IsAttachedToLogicalTree { get; } + /// /// Gets the logical parent. /// diff --git a/src/Perspex.Styling/Perspex.Styling.csproj b/src/Perspex.Styling/Perspex.Styling.csproj index c741543430..2ad2057aa5 100644 --- a/src/Perspex.Styling/Perspex.Styling.csproj +++ b/src/Perspex.Styling/Perspex.Styling.csproj @@ -45,11 +45,11 @@ - + diff --git a/src/Perspex.Styling/Properties/AssemblyInfo.cs b/src/Perspex.Styling/Properties/AssemblyInfo.cs index f06c17710e..21034a0753 100644 --- a/src/Perspex.Styling/Properties/AssemblyInfo.cs +++ b/src/Perspex.Styling/Properties/AssemblyInfo.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; using Perspex.Metadata; [assembly: AssemblyTitle("Perspex.Styling")] -[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")] \ No newline at end of file +[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")] +[assembly: InternalsVisibleTo("Perspex.Styling.UnitTests")] \ No newline at end of file diff --git a/src/Perspex.Styling/Styling/Classes.cs b/src/Perspex.Styling/Styling/Classes.cs deleted file mode 100644 index 1517b06018..0000000000 --- a/src/Perspex.Styling/Styling/Classes.cs +++ /dev/null @@ -1,155 +0,0 @@ -// 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; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Reactive; -using System.Reactive.Subjects; - -namespace Perspex.Styling -{ - public class Classes : ICollection, INotifyCollectionChanged - { - private readonly List _inner; - - private readonly Subject _beforeChanged - = new Subject(); - - private readonly Subject _changed - = new Subject(); - - private readonly Subject _afterChanged - = new Subject(); - - public Classes() - { - _inner = new List(); - } - - public Classes(params string[] classes) - { - _inner = new List(classes); - } - - public Classes(IEnumerable classes) - { - _inner = new List(classes); - } - - public event NotifyCollectionChangedEventHandler CollectionChanged; - - public int Count => _inner.Count; - - public bool IsReadOnly => false; - - public IObservable BeforeChanged => _beforeChanged; - - public IObservable Changed => _changed; - - public IObservable AfterChanged => _afterChanged; - - public void Add(string item) - { - Add(Enumerable.Repeat(item, 1)); - } - - public void Add(params string[] items) - { - Add((IEnumerable)items); - } - - public void Add(IEnumerable items) - { - items = items.Except(_inner); - - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, - items); - - _beforeChanged.OnNext(e); - _inner.AddRange(items); - RaiseChanged(e); - } - - public void Clear() - { - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Reset); - - _beforeChanged.OnNext(e); - _inner.Clear(); - RaiseChanged(e); - } - - public bool Contains(string item) - { - return _inner.Contains(item); - } - - public void CopyTo(string[] array, int arrayIndex) - { - _inner.CopyTo(array, arrayIndex); - } - - public IEnumerator GetEnumerator() - { - return _inner.GetEnumerator(); - } - - public override string ToString() - { - return string.Join(" ", this); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } - - public bool Remove(string item) - { - return Remove(Enumerable.Repeat(item, 1)); - } - - public bool Remove(params string[] items) - { - return Remove((IEnumerable)items); - } - - public bool Remove(IEnumerable items) - { - items = items.Intersect(_inner); - - if (items.Any()) - { - NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, - items); - - _beforeChanged.OnNext(e); - - foreach (string item in items) - { - _inner.Remove(item); - } - - RaiseChanged(e); - return true; - } - else - { - return false; - } - } - - private void RaiseChanged(NotifyCollectionChangedEventArgs e) - { - CollectionChanged?.Invoke(this, e); - _changed.OnNext(e); - _afterChanged.OnNext(e); - } - } -} diff --git a/src/Perspex.Styling/Styling/IGlobalStyles.cs b/src/Perspex.Styling/Styling/IGlobalStyles.cs index ef246f9b84..85cc9abaa7 100644 --- a/src/Perspex.Styling/Styling/IGlobalStyles.cs +++ b/src/Perspex.Styling/Styling/IGlobalStyles.cs @@ -3,8 +3,10 @@ namespace Perspex.Styling { - public interface IGlobalStyles + /// + /// Defines the style host that provides styles global to the application. + /// + public interface IGlobalStyles : IStyleRoot { - Styles Styles { get; } } } diff --git a/src/Perspex.Styling/Styling/IStyleHost.cs b/src/Perspex.Styling/Styling/IStyleHost.cs index 40cad25f63..9f434e1231 100644 --- a/src/Perspex.Styling/Styling/IStyleHost.cs +++ b/src/Perspex.Styling/Styling/IStyleHost.cs @@ -6,7 +6,7 @@ namespace Perspex.Styling /// /// Defines an element that has a collection. /// - public interface IStyleHost : IVisual + public interface IStyleHost { /// /// Gets the styles for the element. diff --git a/src/Perspex.Styling/Styling/IStyleRoot.cs b/src/Perspex.Styling/Styling/IStyleRoot.cs new file mode 100644 index 0000000000..9b3e1dcc70 --- /dev/null +++ b/src/Perspex.Styling/Styling/IStyleRoot.cs @@ -0,0 +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. + +namespace Perspex.Styling +{ + /// + /// Denotes the root in a tree. + /// + public interface IStyleRoot : IStyleHost + { + } +} diff --git a/src/Perspex.Styling/Styling/IStyleable.cs b/src/Perspex.Styling/Styling/IStyleable.cs index c28e96f184..e5a3f9db81 100644 --- a/src/Perspex.Styling/Styling/IStyleable.cs +++ b/src/Perspex.Styling/Styling/IStyleable.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Perspex.Collections; +using System.Reactive; namespace Perspex.Styling { @@ -10,10 +12,15 @@ namespace Perspex.Styling /// public interface IStyleable : IObservablePropertyBag, INamed { + /// + /// Raised when the control's style should be removed. + /// + IObservable StyleDetach { get; } + /// /// Gets the list of classes for the control. /// - Classes Classes { get; } + IPerspexReadOnlyList Classes { get; } /// /// Gets the type by which the control is styled. diff --git a/src/Perspex.Styling/Styling/Selector.cs b/src/Perspex.Styling/Styling/Selector.cs index 06999744c1..b47a3e28fd 100644 --- a/src/Perspex.Styling/Styling/Selector.cs +++ b/src/Perspex.Styling/Styling/Selector.cs @@ -141,7 +141,7 @@ namespace Perspex.Styling if (inputs.Count > 0) { - return new SelectorMatch(new StyleActivator(inputs)); + return new SelectorMatch(StyleActivator.And(inputs)); } else { diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index 65f0d6dee0..7ed066f293 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -3,6 +3,9 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reflection; @@ -61,7 +64,7 @@ namespace Perspex.Styling { Contract.Requires(previous != null); - return new Selector(previous, x => MatchIs(x, type), type.Name, type); + return new Selector(previous, x => MatchIs(x, type), $":is({type.Name})", type); } /// @@ -176,10 +179,16 @@ namespace Perspex.Styling private static SelectorMatch MatchClass(IStyleable control, string name) { - return new SelectorMatch( - Observable - .Return(control.Classes.Contains(name)) - .Concat(control.Classes.Changed.Select(e => control.Classes.Contains(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) @@ -209,9 +218,7 @@ namespace Perspex.Styling } } - return new SelectorMatch(new StyleActivator( - descendentMatches, - ActivatorMode.Or)); + return new SelectorMatch(StyleActivator.Or(descendentMatches)); } private static SelectorMatch MatchIs(IStyleable control, Type type) diff --git a/src/Perspex.Styling/Styling/Style.cs b/src/Perspex.Styling/Styling/Style.cs index 61b05d1412..b1337b0233 100644 --- a/src/Perspex.Styling/Styling/Style.cs +++ b/src/Perspex.Styling/Styling/Style.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using Perspex.Metadata; @@ -13,6 +14,8 @@ namespace Perspex.Styling /// public class Style : IStyle { + private static readonly IObservable True = Observable.Never().StartWith(true); + /// /// Initializes a new instance of the class. /// @@ -56,9 +59,12 @@ namespace Perspex.Styling if (match.ImmediateResult != false) { + var activator = (match.ObservableResult ?? True) + .TakeUntil(control.StyleDetach); + foreach (var setter in Setters) { - setter.Apply(this, control, match.ObservableResult); + setter.Apply(this, control, activator); } } } diff --git a/src/Perspex.Styling/Styling/StyleActivator.cs b/src/Perspex.Styling/Styling/StyleActivator.cs index c6b92a4e39..463fc5bb78 100644 --- a/src/Perspex.Styling/Styling/StyleActivator.cs +++ b/src/Perspex.Styling/Styling/StyleActivator.cs @@ -4,7 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive.Disposables; +using System.Reactive; +using System.Reactive.Linq; namespace Perspex.Styling { @@ -14,134 +15,28 @@ namespace Perspex.Styling Or, } - public class StyleActivator : IObservable, IDisposable + public static class StyleActivator { - private readonly ActivatorMode _mode; - - private readonly bool[] _values; - - private readonly List _subscriptions = new List(); - - private readonly List> _observers = new List>(); - - public StyleActivator( - IList> inputs, - ActivatorMode mode = ActivatorMode.And) - { - int i = 0; - - _mode = mode; - _values = new bool[inputs.Count]; - - foreach (IObservable input in inputs) - { - int capturedIndex = i; - - IDisposable subscription = input.Subscribe( - x => Update(capturedIndex, x), - x => Finish(capturedIndex), - () => Finish(capturedIndex)); - _subscriptions.Add(subscription); - ++i; - } - } - - public bool CurrentValue - { - get; - private set; - } - - public bool HasCompleted - { - get; - private set; - } - - public void Dispose() - { - foreach (IObserver observer in _observers) - { - observer.OnCompleted(); - } - - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } - } - - public IDisposable Subscribe(IObserver observer) - { - Contract.Requires(observer != null); - - observer.OnNext(CurrentValue); - - if (HasCompleted) - { - observer.OnCompleted(); - return Disposable.Empty; - } - else - { - _observers.Add(observer); - return Disposable.Create(() => _observers.Remove(observer)); - } - } - - private void Update(int index, bool value) - { - _values[index] = value; - - bool current; - - switch (_mode) - { - case ActivatorMode.And: - current = _values.All(x => x); - break; - case ActivatorMode.Or: - current = _values.Any(x => x); - break; - default: - throw new InvalidOperationException("Invalid Activator mode."); - } - - if (current != CurrentValue) - { - Push(current); - CurrentValue = current; - } - } - - private void Finish(int i) + public static IObservable And(IEnumerable> inputs) { - // We can unsubscribe from everything if the completed observable: - // - Is the only subscription. - // - Has finished on 'false' and we're in And mode - // - Has finished on 'true' and we're in Or mode - var value = _values[i]; - var unsubscribe = - (_values.Length == 1) || - (_mode == ActivatorMode.And ? !value : value); + var sourceArray = inputs.Select(s => s.Publish().RefCount()).ToArray(); - if (unsubscribe) - { - foreach (IDisposable subscription in _subscriptions) - { - subscription.Dispose(); - } + var terminate = sourceArray + .ToObservable() + .SelectMany(x => x.LastAsync() + .Where(y => y == false)); - HasCompleted = true; - } + return sourceArray + .CombineLatest(values => values.All(x => x)) + .DistinctUntilChanged() + .TakeUntil(terminate); } - private void Push(bool value) + public static IObservable Or(IEnumerable> inputs) { - foreach (IObserver observer in _observers) - { - observer.OnNext(value); - } + return inputs.CombineLatest() + .Select(values => values.Any(x => x)) + .DistinctUntilChanged(); } } } diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs index 9eb6f00235..b2ddbeee1b 100644 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ b/src/Perspex.Styling/Styling/StyleBinding.cs @@ -61,7 +61,8 @@ namespace Perspex.Styling /// public object ActivatedValue { - get; } + get; + } /// /// Gets a description of the binding. @@ -90,16 +91,16 @@ namespace Perspex.Styling if (Source == null) { - return _activator.Subscribe( - active => observer.OnNext(active ? ActivatedValue : PerspexProperty.UnsetValue), - observer.OnError, - observer.OnCompleted); + return _activator + .Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue) + .Subscribe(observer); } else { return _activator .CombineLatest(Source, (x, y) => new { Active = x, Value = y }) - .Subscribe(x => observer.OnNext(x.Active ? x.Value : PerspexProperty.UnsetValue)); + .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue) + .Subscribe(observer); } } } diff --git a/src/Perspex.Styling/Styling/Styler.cs b/src/Perspex.Styling/Styling/Styler.cs index f2f8f56380..400af18252 100644 --- a/src/Perspex.Styling/Styling/Styler.cs +++ b/src/Perspex.Styling/Styling/Styler.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; -using Perspex.VisualTree; namespace Perspex.Styling { @@ -11,39 +9,27 @@ namespace Perspex.Styling { public void ApplyStyles(IStyleable control) { - IVisual visual = control as IVisual; - IStyleHost styleContainer = visual - .GetSelfAndVisualAncestors() - .OfType() - .FirstOrDefault(); - IGlobalStyles global = PerspexLocator.Current.GetService(); + var styleHost = control as IStyleHost; - global?.Styles.Attach(control, null); - - if (styleContainer != null) + if (styleHost != null) { - ApplyStyles(control, styleContainer); + ApplyStyles(control, styleHost); } } - private void ApplyStyles(IStyleable control, IStyleHost container) + private void ApplyStyles(IStyleable control, IStyleHost styleHost) { Contract.Requires(control != null); - Contract.Requires(container != null); + Contract.Requires(styleHost != null); - var parentContainer = container.StylingParent; + var parentContainer = styleHost.StylingParent; if (parentContainer != null) { ApplyStyles(control, parentContainer); } - container.Styles.Attach(control, container); - } - - private IStyleHost GetParentContainer(IStyleHost container) - { - return container.GetVisualAncestors().OfType().FirstOrDefault(); + styleHost.Styles.Attach(control, styleHost); } } } diff --git a/src/Perspex.Themes.Default/Button.paml b/src/Perspex.Themes.Default/Button.paml index 256ac94df9..7cfd1e7f35 100644 --- a/src/Perspex.Themes.Default/Button.paml +++ b/src/Perspex.Themes.Default/Button.paml @@ -13,7 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - - + + diff --git a/src/Perspex.Themes.Default/Expander.paml b/src/Perspex.Themes.Default/Expander.paml new file mode 100644 index 0000000000..158f6514fd --- /dev/null +++ b/src/Perspex.Themes.Default/Expander.paml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Perspex.Themes.Default/MenuItem.paml b/src/Perspex.Themes.Default/MenuItem.paml index 68d4602a92..b9ba42eac5 100644 --- a/src/Perspex.Themes.Default/MenuItem.paml +++ b/src/Perspex.Themes.Default/MenuItem.paml @@ -24,7 +24,8 @@ IsVisible="False" Margin="3" VerticalAlignment="Center"/> - @@ -44,22 +45,22 @@ PlacementMode="Right" StaysOpen="True" IsOpen="{TemplateBinding Path=IsSubMenuOpen, Mode=TwoWay}"> - + - @@ -78,7 +79,8 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - @@ -89,22 +91,22 @@ - + - diff --git a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj index 2ec676592d..cfc8544d16 100644 --- a/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj +++ b/src/Perspex.Themes.Default/Perspex.Themes.Default.csproj @@ -143,6 +143,7 @@ Designer + Designer @@ -159,7 +160,7 @@ Designer - + Designer diff --git a/src/Perspex.Themes.Default/TabControl.paml b/src/Perspex.Themes.Default/TabControl.paml index 0809be9077..b678304a87 100644 --- a/src/Perspex.Themes.Default/TabControl.paml +++ b/src/Perspex.Themes.Default/TabControl.paml @@ -3,10 +3,13 @@ - - @@ -18,11 +21,14 @@ - - @@ -35,7 +41,9 @@ - @@ -45,8 +53,9 @@ - @@ -58,13 +67,16 @@ - - diff --git a/src/Perspex.Themes.Default/TabStrip.paml b/src/Perspex.Themes.Default/TabStrip.paml index a9df7c0ca7..fbc8aefd8d 100644 --- a/src/Perspex.Themes.Default/TabStrip.paml +++ b/src/Perspex.Themes.Default/TabStrip.paml @@ -2,7 +2,9 @@ - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/TabItem.paml b/src/Perspex.Themes.Default/TabStripItem.paml similarity index 53% rename from src/Perspex.Themes.Default/TabItem.paml rename to src/Perspex.Themes.Default/TabStripItem.paml index 3781d32de7..08dd0bb932 100644 --- a/src/Perspex.Themes.Default/TabItem.paml +++ b/src/Perspex.Themes.Default/TabStripItem.paml @@ -1,14 +1,16 @@  - - \ No newline at end of file diff --git a/src/Perspex.Themes.Default/ToggleButton.paml b/src/Perspex.Themes.Default/ToggleButton.paml index cbb5341450..0311294db9 100644 --- a/src/Perspex.Themes.Default/ToggleButton.paml +++ b/src/Perspex.Themes.Default/ToggleButton.paml @@ -13,7 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - - diff --git a/tests/Perspex.Controls.UnitTests/ClassesTests.cs b/tests/Perspex.Controls.UnitTests/ClassesTests.cs new file mode 100644 index 0000000000..9669f4ea9c --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/ClassesTests.cs @@ -0,0 +1,153 @@ +// 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 Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class ClassesTests + { + [Fact] + public void Duplicates_Should_Not_Be_Added() + { + var target = new Classes(); + + target.Add("foo"); + target.Add("foo"); + + Assert.Equal(new[] { "foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Added_Via_AddRange() + { + var target = new Classes(); + + target.Add("foo"); + target.AddRange(new[] { "foo", "bar" }); + + Assert.Equal(new[] { "foo", "bar" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Added_Via_Pseudoclasses() + { + var target = new Classes(); + var ps = (IPseudoClasses)target; + + ps.Add(":foo"); + ps.Add(":foo"); + + Assert.Equal(new[] { ":foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Inserted() + { + var target = new Classes(); + + target.Add("foo"); + target.Insert(0, "foo"); + + Assert.Equal(new[] { "foo" }, target); + } + + [Fact] + public void Duplicates_Should_Not_Be_Inserted_Via_InsertRange() + { + var target = new Classes(); + + target.Add("foo"); + target.InsertRange(1, new[] { "foo", "bar" }); + + Assert.Equal(new[] { "foo", "bar" }, target); + } + + [Fact] + public void Should_Not_Be_Able_To_Add_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Add(":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Add_Pseudoclasses_Via_AddRange() + { + var target = new Classes(); + + Assert.Throws(() => target.AddRange(new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Insert_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Insert(0, ":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Insert_Pseudoclasses_Via_InsertRange() + { + var target = new Classes(); + + Assert.Throws(() => target.InsertRange(0, new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclass() + { + var target = new Classes(); + + Assert.Throws(() => target.Remove(":foo")); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveAll() + { + var target = new Classes(); + + Assert.Throws(() => target.RemoveAll(new[] { "foo", ":bar" })); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclasses_Via_RemoveRange() + { + var target = new Classes(); + + Assert.Throws(() => target.RemoveRange(0, 1)); + } + + [Fact] + public void Should_Not_Be_Able_To_Remove_Pseudoclass_Via_RemoveAt() + { + var target = new Classes(); + + ((IPseudoClasses)target).Add(":foo"); + + Assert.Throws(() => target.RemoveAt(0)); + } + + [Fact] + public void Replace_Should_Not_Replace_Pseudoclasses() + { + var target = new Classes("foo", "bar"); + + ((IPseudoClasses)target).Add(":baz"); + + target.Replace(new[] { "qux" }); + + Assert.Equal(new[] { ":baz", "qux" }, target); + } + + [Fact] + public void Replace_Should_Not_Accept_Pseudoclasses() + { + var target = new Classes(); + + Assert.Throws(() => target.Replace(new[] { ":qux" })); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs b/tests/Perspex.Controls.UnitTests/ContentControlTests.cs index ca47bf8571..ff0c993af6 100644 --- a/tests/Perspex.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ContentControlTests.cs @@ -90,7 +90,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void Setting_Content_To_Control_Should_Set_Child_Controls_Parent() + public void Control_Content_Should_Be_Logical_Child_Before_ApplyTemplate() { var target = new ContentControl { @@ -99,14 +99,14 @@ namespace Perspex.Controls.UnitTests var child = new Control(); target.Content = child; - target.ApplyTemplate(); Assert.Equal(child.Parent, target); - Assert.Equal(((ILogical)child).LogicalParent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); } [Fact] - public void Setting_Content_To_String_Should_Set_Child_Controls_Parent() + public void DataTemplate_Created_Control_Should_Be_Logical_Child_After_ApplyTemplate() { var target = new ContentControl { @@ -118,12 +118,14 @@ namespace Perspex.Controls.UnitTests var child = target.Presenter.Child; - Assert.Equal(child.Parent, target); - Assert.Equal(((ILogical)child).LogicalParent, target); + Assert.NotNull(child); + Assert.Equal(target, child.Parent); + Assert.Equal(target, child.GetLogicalParent()); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); } [Fact] - public void Clearing_Content_Should_Clear_Child_Controls_Parent() + public void Clearing_Content_Should_Clear_Logical_Child() { var target = new ContentControl(); var child = new Control(); @@ -132,52 +134,7 @@ namespace Perspex.Controls.UnitTests target.Content = null; Assert.Null(child.Parent); - Assert.Null(((ILogical)child).LogicalParent); - } - - [Fact] - public void Setting_Content_To_Control_Should_Make_Control_Appear_In_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = child; - target.ApplyTemplate(); - - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Setting_Content_To_String_Should_Make_TextBlock_Appear_In_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = "Foo"; - target.ApplyTemplate(); - - var logical = (ILogical)target; - Assert.Equal(1, logical.LogicalChildren.Count); - Assert.IsType(logical.LogicalChildren[0]); - } - - [Fact] - public void Clearing_Content_Should_Remove_From_LogicalChildren() - { - var target = new ContentControl(); - var child = new Control(); - - target.Template = GetTemplate(); - target.Content = child; - target.ApplyTemplate(); - - target.Content = null; - - // Need to call ApplyTemplate on presenter for LogicalChildren to be updated. - target.Presenter.ApplyTemplate(); - + Assert.Null(child.GetLogicalParent()); Assert.Empty(target.GetLogicalChildren()); } @@ -194,8 +151,6 @@ namespace Perspex.Controls.UnitTests target.Template = GetTemplate(); target.Content = child; target.ApplyTemplate(); - - // Need to call ApplyTemplate on presenter for LogicalChildren to be updated. target.Presenter.ApplyTemplate(); Assert.True(called); @@ -215,8 +170,6 @@ namespace Perspex.Controls.UnitTests ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; target.Content = null; - - // Need to call ApplyTemplate on presenter for CollectionChanged to be called. target.Presenter.ApplyTemplate(); Assert.True(called); @@ -237,8 +190,6 @@ namespace Perspex.Controls.UnitTests ((ILogical)contentControl).LogicalChildren.CollectionChanged += (s, e) => called = true; contentControl.Content = child2; - - // Need to call ApplyTemplate on presenter for CollectionChanged to be called. contentControl.Presenter.ApplyTemplate(); Assert.True(called); @@ -261,7 +212,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void DataContext_Should_Be_Set_For_Templated_Data() + public void DataContext_Should_Be_Set_For_DataTemplate_Created_Content() { var target = new ContentControl(); @@ -273,7 +224,7 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void DataContext_Should_Not_Be_Set_For_Control_Data() + public void DataContext_Should_Not_Be_Set_For_Control_Content() { var target = new ContentControl(); diff --git a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs deleted file mode 100644 index 05b330872f..0000000000 --- a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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.Collections.Specialized; -using System.Linq; -using Perspex.Controls.Presenters; -using Perspex.LogicalTree; -using Xunit; - -namespace Perspex.Controls.UnitTests -{ - public class ContentPresenterTests - { - [Fact] - public void Setting_Content_Should_Make_Control_Appear_In_LogicalChildren() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Clearing_Content_Should_Remove_From_LogicalChildren() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - target.Content = null; - target.ApplyTemplate(); - - Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); - } - - [Fact] - public void Clearing_Content_Clear_Childs_Parent() - { - var target = new ContentPresenter(); - var child = new Control(); - - target.Content = child; - target.ApplyTemplate(); - target.Content = null; - target.ApplyTemplate(); - - Assert.Null(child.Parent); - Assert.Null(child.GetLogicalParent()); - } - - [Fact] - public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ContentPresenter(); - var child = new Control(); - var called = false; - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => - called = e.Action == NotifyCollectionChangedAction.Add; - - target.Content = child; - target.ApplyTemplate(); - - Assert.True(called); - } - - [Fact] - public void Clearing_Content_Should_Fire_LogicalChildren_CollectionChanged() - { - var target = new ContentPresenter(); - var child = new Control(); - var called = false; - - target.Content = child; - target.ApplyTemplate(); - - ((ILogical)target).LogicalChildren.CollectionChanged += (s, e) => called = true; - - target.Content = null; - target.ApplyTemplate(); - - Assert.True(called); - } - } -} diff --git a/tests/Perspex.Controls.UnitTests/ControlTests.cs b/tests/Perspex.Controls.UnitTests/ControlTests.cs index d86733de3d..152c175d20 100644 --- a/tests/Perspex.Controls.UnitTests/ControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ControlTests.cs @@ -2,6 +2,8 @@ // 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.Reactive.Linq; using Moq; using Perspex.Layout; using Perspex.Platform; @@ -22,24 +24,105 @@ namespace Perspex.Controls.UnitTests } [Fact] - public void Adding_Control_To_IRenderRoot_Should_Style_Control() + public void LogicalParent_Should_Be_Set_To_Parent() { - using (PerspexLocator.EnterScope()) + var parent = new Decorator(); + var target = new TestControl(); + + parent.Child = target; + + Assert.Equal(parent, target.InheritanceParent); + } + + [Fact] + public void LogicalParent_Should_Be_Cleared_When_Removed_From_Parent() + { + var parent = new Decorator(); + var target = new TestControl(); + + parent.Child = target; + parent.Child = null; + + Assert.Null(target.InheritanceParent); + } + + [Fact] + public void AttachedToLogicalParent_Should_Be_Called_When_Added_To_Tree() + { + var root = new TestRoot(); + var parent = new Border(); + var child = new Border(); + var grandchild = new Border(); + var parentRaised = false; + var childRaised = false; + var grandchildRaised = false; + + parent.AttachedToLogicalTree += (s, e) => parentRaised = true; + child.AttachedToLogicalTree += (s, e) => childRaised = true; + grandchild.AttachedToLogicalTree += (s, e) => grandchildRaised = true; + + parent.Child = child; + child.Child = grandchild; + + Assert.False(parentRaised); + Assert.False(childRaised); + Assert.False(grandchildRaised); + + root.Child = parent; + + Assert.True(parentRaised); + Assert.True(childRaised); + Assert.True(grandchildRaised); + } + + [Fact] + public void AttachedToLogicalParent_Should_Be_Called_Before_Parent_Change_Signalled() + { + var root = new TestRoot(); + var child = new Border(); + var raised = new List(); + + child.AttachedToLogicalTree += (s, e) => { - var root = new TestRoot(); - var target = new Control(); - var styler = new Mock(); + Assert.Equal(root, child.Parent); + raised.Add("attached"); + }; - PerspexLocator.CurrentMutable.Bind().ToConstant(styler.Object); + child.GetObservable(Control.ParentProperty).Skip(1).Subscribe(_ => raised.Add("parent")); - root.Child = target; + root.Child = child; - styler.Verify(x => x.ApplyStyles(target), Times.Once()); - } + Assert.Equal(new[] { "attached", "parent" }, raised); } [Fact] - public void Adding_Tree_To_ILayoutRoot_Should_Style_Controls() + public void DetachedToLogicalParent_Should_Be_Called_When_Removed_From_Tree() + { + var root = new TestRoot(); + var parent = new Border(); + var child = new Border(); + var grandchild = new Border(); + var parentRaised = false; + var childRaised = false; + var grandchildRaised = false; + + parent.Child = child; + child.Child = grandchild; + root.Child = parent; + + parent.DetachedFromLogicalTree += (s, e) => parentRaised = true; + child.DetachedFromLogicalTree += (s, e) => childRaised = true; + grandchild.DetachedFromLogicalTree += (s, e) => grandchildRaised = true; + + root.Child = null; + + Assert.True(parentRaised); + Assert.True(childRaised); + Assert.True(grandchildRaised); + } + + [Fact] + public void Adding_Tree_To_IStyleRoot_Should_Style_Controls() { using (PerspexLocator.EnterScope()) { @@ -64,7 +147,7 @@ namespace Perspex.Controls.UnitTests } } - private class TestRoot : Decorator, ILayoutRoot, IRenderRoot + private class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot { public Size ClientSize { @@ -91,5 +174,10 @@ namespace Perspex.Controls.UnitTests throw new NotImplementedException(); } } + + private class TestControl : Control + { + public new PerspexObject InheritanceParent => base.InheritanceParent; + } } } diff --git a/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs b/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs index 2aa911b61a..ec5f657877 100644 --- a/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs +++ b/tests/Perspex.Controls.UnitTests/ControlTests_NameScope.cs @@ -5,6 +5,7 @@ using System; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.Rendering; +using Perspex.Styling; using Xunit; namespace Perspex.Controls.UnitTests @@ -71,7 +72,7 @@ namespace Perspex.Controls.UnitTests Assert.Null(NameScope.GetNameScope(root.Presenter).Find("foo")); } - private class TestRoot : ContentControl, IRenderRoot, INameScope + private class TestRoot : ContentControl, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs b/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs new file mode 100644 index 0000000000..92b7f9c815 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/EnumerableExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex.Controls.UnitTests +{ + internal static class EnumerableExtensions + { + public static IEnumerable Do(this IEnumerable items, Action action) + { + foreach (var i in items) + { + action(i); + yield return i; + } + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index d8d2c6ba1f..7731155e71 100644 --- a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -16,7 +16,11 @@ namespace Perspex.Controls.UnitTests.Generators var owner = new Decorator(); var target = new ItemContainerGenerator(owner); var containers = target.Materialize(0, items, null); - var result = containers.OfType().Select(x => x.Text).ToList(); + var result = containers + .Select(x => x.ContainerControl) + .OfType() + .Select(x => x.Text) + .ToList(); Assert.Equal(items, result); } @@ -29,9 +33,9 @@ namespace Perspex.Controls.UnitTests.Generators var target = new ItemContainerGenerator(owner); var containers = target.Materialize(0, items, null).ToList(); - Assert.Equal(containers[0], target.ContainerFromIndex(0)); - Assert.Equal(containers[1], target.ContainerFromIndex(1)); - Assert.Equal(containers[2], target.ContainerFromIndex(2)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); + Assert.Equal(containers[1].ContainerControl, target.ContainerFromIndex(1)); + Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } [Fact] @@ -42,9 +46,9 @@ namespace Perspex.Controls.UnitTests.Generators var target = new ItemContainerGenerator(owner); var containers = target.Materialize(0, items, null).ToList(); - Assert.Equal(0, target.IndexFromContainer(containers[0])); - Assert.Equal(1, target.IndexFromContainer(containers[1])); - Assert.Equal(2, target.IndexFromContainer(containers[2])); + Assert.Equal(0, target.IndexFromContainer(containers[0].ContainerControl)); + Assert.Equal(1, target.IndexFromContainer(containers[1].ContainerControl)); + Assert.Equal(2, target.IndexFromContainer(containers[2].ContainerControl)); } [Fact] @@ -57,9 +61,9 @@ namespace Perspex.Controls.UnitTests.Generators target.Dematerialize(1, 1); - Assert.Equal(containers[0], target.ContainerFromIndex(0)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); Assert.Equal(null, target.ContainerFromIndex(1)); - Assert.Equal(containers[2], target.ContainerFromIndex(2)); + Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } [Fact] @@ -85,8 +89,8 @@ namespace Perspex.Controls.UnitTests.Generators var removed = target.RemoveRange(1, 1).Single(); - Assert.Equal(containers[0], target.ContainerFromIndex(0)); - Assert.Equal(containers[2], target.ContainerFromIndex(1)); + Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); + Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(1)); Assert.Equal(containers[1], removed); } } diff --git a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs index f6b0ff2eae..e59a1e0256 100644 --- a/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Perspex.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -17,10 +17,10 @@ namespace Perspex.Controls.UnitTests.Generators var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty); var containers = target.Materialize(0, items, null); var result = containers + .Select(x => x.ContainerControl) .OfType() .Select(x => x.Content) - .OfType() - .Select(x => x.Text).ToList(); + .ToList(); Assert.Equal(items, result); } diff --git a/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs new file mode 100644 index 0000000000..3eb4db00de --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -0,0 +1,78 @@ +// 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 Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Perspex.Controls.Templates; +using Perspex.LogicalTree; +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class HeaderedItemsControlTests + { + [Fact] + public void Control_Header_Should_Be_Logical_Child_Before_ApplyTemplate() + { + var target = new HeaderedItemsControl + { + Template = GetTemplate(), + }; + + var child = new Control(); + target.Header = child; + + Assert.Equal(child.Parent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void DataTemplate_Created_Control_Should_Be_Logical_Child_After_ApplyTemplate() + { + var target = new HeaderedItemsControl + { + Template = GetTemplate(), + }; + + target.Header = "Foo"; + target.ApplyTemplate(); + + var child = target.HeaderPresenter.Child; + + Assert.NotNull(child); + Assert.Equal(target, child.Parent); + Assert.Equal(target, child.GetLogicalParent()); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void Clearing_Content_Should_Clear_Logical_Child() + { + var target = new HeaderedItemsControl(); + var child = new Control(); + + target.Header = child; + target.Header = null; + + Assert.Null(child.Parent); + Assert.Null(child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Child = new ContentPresenter + { + Name = "PART_HeaderPresenter", + [~ContentPresenter.ContentProperty] = parent[~HeaderedItemsControl.HeaderProperty], + } + }; + }); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs index e066866a05..9cc960a5c2 100644 --- a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs @@ -7,7 +7,6 @@ using Perspex.Collections; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.LogicalTree; -using Perspex.Styling; using Perspex.VisualTree; using Xunit; @@ -24,10 +23,7 @@ namespace Perspex.Controls.UnitTests target.Items = new[] { "Foo" }; target.ApplyTemplate(); - var presenter = target.GetTemplateChildren().OfType().Single(); - var panel = target.GetTemplateChildren().OfType().Single(); - - Assert.Equal(target, panel.TemplatedParent); + Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } [Fact] @@ -39,29 +35,57 @@ namespace Perspex.Controls.UnitTests target.Items = new[] { "Foo" }; target.ApplyTemplate(); - var presenter = target.GetTemplateChildren().OfType().Single(); - var panel = target.GetTemplateChildren().OfType().Single(); - var item = (TextBlock)panel.GetVisualChildren().First(); + var item = (TextBlock)target.Presenter.Panel.GetVisualChildren().First(); Assert.Null(item.TemplatedParent); } [Fact] - public void Control_Item_Should_Have_Parent_Set() + public void Control_Item_Should_Be_Logical_Child_Before_ApplyTemplate() { var target = new ItemsControl(); var child = new Control(); target.Template = GetTemplate(); target.Items = new[] { child }; - target.ApplyTemplate(); - Assert.Equal(target, child.Parent); - Assert.Equal(target, ((ILogical)child).LogicalParent); + Assert.Equal(child.Parent, target); + Assert.Equal(child.GetLogicalParent(), target); + Assert.Equal(new[] { child }, target.GetLogicalChildren()); + } + + [Fact] + public void Control_Item_Should_Be_Removed_From_Logical_Children_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + var items = new PerspexList(child); + + target.Template = GetTemplate(); + target.Items = items; + items.RemoveAt(0); + + Assert.Null(child.Parent); + Assert.Null(child.GetLogicalParent()); + Assert.Empty(target.GetLogicalChildren()); } [Fact] - public void Clearing_Control_Item_Should_Clear_Child_Controls_Parent() + public void Clearing_Items_Should_Clear_Child_Controls_Parent_Before_ApplyTemplate() + { + var target = new ItemsControl(); + var child = new Control(); + + target.Template = GetTemplate(); + target.Items = new[] { child }; + target.Items = null; + + Assert.Null(child.Parent); + Assert.Null(((ILogical)child).LogicalParent); + } + + [Fact] + public void Clearing_Items_Should_Clear_Child_Controls_Parent() { var target = new ItemsControl(); var child = new Control(); @@ -83,9 +107,13 @@ namespace Perspex.Controls.UnitTests target.Template = GetTemplate(); target.Items = new[] { child }; + + // Should appear both before and after applying template. + Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); + target.ApplyTemplate(); - Assert.Equal(new[] { child }, ((ILogical)target).LogicalChildren.ToList()); + Assert.Equal(new ILogical[] { child }, target.GetLogicalChildren()); } [Fact] @@ -114,7 +142,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); target.Items = null; - Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); + Assert.Equal(new ILogical[0], target.GetLogicalChildren()); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs index 0f1a60b902..08c445e8df 100644 --- a/tests/Perspex.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Perspex.Controls.UnitTests/ListBoxTests.cs @@ -14,7 +14,31 @@ namespace Perspex.Controls.UnitTests public class ListBoxTests { [Fact] - public void LogicalChildren_Should_Be_Set() + public void ListBoxItem_Containers_Should_Be_Generated() + { + var items = new[] { "Foo", "Bar", "Baz " }; + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + Items = items, + }; + + target.ApplyTemplate(); + + var text = target.Presenter.Panel.Children + .OfType() + .Do(x => x.Template = ListBoxItemTemplate()) + .Do(x => x.ApplyTemplate()) + .Select(x => x.Presenter.Child) + .OfType() + .Select(x => x.Text) + .ToList(); + + Assert.Equal(items, text); + } + + [Fact] + public void LogicalChildren_Should_Be_Set_For_DataTemplate_Generated_Items() { var target = new ListBox { @@ -103,6 +127,15 @@ namespace Perspex.Controls.UnitTests }; } + private FuncControlTemplate ListBoxItemTemplate() + { + return new FuncControlTemplate(parent => new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], + }); + } + private Control CreateScrollViewerTemplate(ITemplatedControl parent) { return new ScrollContentPresenter diff --git a/tests/Perspex.Controls.UnitTests/PanelTests.cs b/tests/Perspex.Controls.UnitTests/PanelTests.cs index 877f7039fc..68f6075e2e 100644 --- a/tests/Perspex.Controls.UnitTests/PanelTests.cs +++ b/tests/Perspex.Controls.UnitTests/PanelTests.cs @@ -118,23 +118,6 @@ namespace Perspex.Controls.UnitTests Assert.Equal(new ILogical[0], panel.GetLogicalChildren()); } - [Fact] - public void Should_Be_Able_To_Reparent_Child_Controls() - { - var target = new Panel(); - var parent = new TestReparent(); - var control1 = new Control(); - var control2 = new Control(); - - target.Children.Add(control1); - ((IReparentingControl)target).ReparentLogicalChildren(parent, parent.LogicalChildren); - target.Children.Add(control2); - - Assert.Equal(new[] { control1, control2 }, parent.LogicalChildren); - Assert.Equal(parent, target.Children[0].Parent); - Assert.Equal(parent, target.Children[1].Parent); - } - private class TestReparent : Panel { public new IPerspexList LogicalChildren => base.LogicalChildren; diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 8f6cca978e..5b33890f2a 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -89,11 +89,12 @@ - + + diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.cs new file mode 100644 index 0000000000..3bc0465dc4 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Presenters/ContentPresenterTests.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 Perspex.Controls.Presenters; +using Perspex.Controls.Templates; +using Xunit; + +namespace Perspex.Controls.UnitTests.Presenters +{ + public class ContentPresenterTests + { + [Fact] + public void Setting_Content_To_Control_Should_Set_Child() + { + var target = new ContentPresenter(); + var child = new Border(); + + target.Content = child; + + // Child should not update until ApplyTemplate called. + Assert.Null(target.Child); + + target.ApplyTemplate(); + + Assert.Equal(child, target.Child); + } + + [Fact] + public void Setting_Content_To_String_Should_Create_TextBlock() + { + var target = new ContentPresenter(); + + target.Content = "Foo"; + + // Child should not update until ApplyTemplate called. + Assert.Null(target.Child); + + target.ApplyTemplate(); + + Assert.IsType(target.Child); + Assert.Equal("Foo", ((TextBlock)target.Child).Text); + } + + [Fact] + public void Adding_To_Logical_Tree_Should_Reevaluate_DataTemplates() + { + var target = new ContentPresenter + { + Content = "Foo", + }; + + target.ApplyTemplate(); + Assert.IsType(target.Child); + + var root = new TestRoot + { + DataTemplates = new DataTemplates + { + new FuncDataTemplate(x => new Decorator()), + }, + }; + + root.Child = target; + target.ApplyTemplate(); + Assert.IsType(target.Child); + } + } +} diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index db9e6d3aec..b5fc0a0869 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -206,7 +206,8 @@ namespace Perspex.Controls.UnitTests.Presenters target.ApplyTemplate(); - Assert.Equal(panel, target.Panel); + Assert.Same(panel, target.Panel); + Assert.Same(target, target.Panel.Parent); } [Fact] diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs index eb913b3410..adb4bfd0fa 100644 --- a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs @@ -142,6 +142,35 @@ namespace Perspex.Controls.UnitTests.Presenters Assert.Equal(new Rect(-25, -25, 150, 150), content.Bounds); } + [Fact] + public void Measure_Should_Pass_Bounded_X_If_CannotScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + [ScrollContentPresenter.CanScrollHorizontallyProperty] = false, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, double.PositiveInfinity), child.AvailableSize); + } + + [Fact] + public void Measure_Should_Pass_Unbounded_X_If_CanScrollHorizontally() + { + var child = new TestControl(); + var target = new ScrollContentPresenter + { + Content = child, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(Size.Infinity, child.AvailableSize); + } + [Fact] public void Arrange_Should_Set_Viewport_And_Extent_In_That_Order() { @@ -240,8 +269,11 @@ namespace Perspex.Controls.UnitTests.Presenters private class TestControl : Control { + public Size AvailableSize { get; private set; } + protected override Size MeasureOverride(Size availableSize) { + AvailableSize = availableSize; return new Size(150, 150); } } diff --git a/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs new file mode 100644 index 0000000000..0dc1c31462 --- /dev/null +++ b/tests/Perspex.Controls.UnitTests/Presenters/ScrollContentPresenterTests_IScrollable.cs @@ -0,0 +1,216 @@ +// 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 Perspex.Controls.Presenters; +using Perspex.Controls.Primitives; +using Xunit; + +namespace Perspex.Controls.UnitTests +{ + public class ScrollContentPresenterTests_IScrollable + { + [Fact] + public void Measure_Should_Pass_Unchanged_Bounds_To_IScrollable() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.Measure(new Size(100, 100)); + + Assert.Equal(new Size(100, 100), scrollable.AvailableSize); + } + + [Fact] + public void Arrange_Should_Not_Offset_IScrollable_Bounds() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable, + }; + + target.Measure(new Size(100, 100)); + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.Equal(new Rect(0, 0, 100, 100), scrollable.Bounds); + } + + [Fact] + public void Arrange_Should_Not_Set_Viewport_And_Extent_With_IScrollable() + { + var target = new ScrollContentPresenter + { + Content = new TestScrollable() + }; + + var changed = false; + + target.Measure(new Size(100, 100)); + + target.GetObservable(ScrollViewer.ViewportProperty).Skip(1).Subscribe(_ => changed = true); + target.GetObservable(ScrollViewer.ExtentProperty).Skip(1).Subscribe(_ => changed = true); + + target.Arrange(new Rect(0, 0, 100, 100)); + + Assert.False(changed); + } + + [Fact] + public void InvalidateScroll_Should_Be_Set_When_Set_As_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + Assert.NotNull(scrollable.InvalidateScroll); + } + + [Fact] + public void InvalidateScroll_Should_Be_Cleared_When_Removed_From_Content() + { + var scrollable = new TestScrollable(); + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + target.Content = null; + target.ApplyTemplate(); + + Assert.Null(scrollable.InvalidateScroll); + } + + [Fact] + public void Extent_Offset_And_Viewport_Should_Be_Read_From_IScrollable() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + Viewport = new Size(25, 25), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + + scrollable.Extent = new Size(200, 200); + scrollable.Offset = new Vector(100, 100); + scrollable.Viewport = new Size(50, 50); + + Assert.Equal(scrollable.Extent, target.Extent); + Assert.Equal(scrollable.Offset, target.Offset); + Assert.Equal(scrollable.Viewport, target.Viewport); + } + + [Fact] + public void Offset_Should_Be_Written_To_IScrollable() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.ApplyTemplate(); + + target.Offset = new Vector(25, 25); + + Assert.Equal(target.Offset, scrollable.Offset); + } + + [Fact] + public void Offset_Should_Not_Be_Written_To_IScrollable_After_Removal() + { + var scrollable = new TestScrollable + { + Extent = new Size(100, 100), + Offset = new Vector(50, 50), + }; + + var target = new ScrollContentPresenter + { + Content = scrollable + }; + + target.Content = null; + target.Offset = new Vector(25, 25); + + Assert.Equal(new Vector(50, 50), scrollable.Offset); + } + + private class TestScrollable : Control, IScrollable + { + private Size _extent; + private Vector _offset; + private Size _viewport; + + public Size AvailableSize { get; private set; } + public Action InvalidateScroll { get; set; } + + public Size Extent + { + get { return _extent; } + set + { + _extent = value; + InvalidateScroll?.Invoke(); + } + } + + public Vector Offset + { + get { return _offset; } + set + { + _offset = value; + InvalidateScroll?.Invoke(); + } + } + + public Size Viewport + { + get { return _viewport; } + set + { + _viewport = value; + InvalidateScroll?.Invoke(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + AvailableSize = availableSize; + return new Size(150, 150); + } + } + } +} \ No newline at end of file diff --git a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs index 172d2320f1..4751ffbfde 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/PopupTests.cs @@ -183,21 +183,6 @@ namespace Perspex.Controls.UnitTests.Primitives } } - [Fact] - public void PopupRoot_Should_Have_Child_As_LogicalChild() - { - using (CreateServices()) - { - var target = new Popup(); - var child = new Control(); - - target.Child = child; - target.Open(); - - Assert.Equal(new[] { child }, target.PopupRoot.GetLogicalChildren()); - } - } - [Fact] public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent() { @@ -263,7 +248,6 @@ namespace Perspex.Controls.UnitTests.Primitives var globalStyles = new Mock(); globalStyles.Setup(x => x.Styles).Returns(styles); - PerspexLocator.CurrentMutable .Bind().ToTransient() .Bind().ToFunc(() => globalStyles.Object) diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs index d20b15be23..6ae55f73c2 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs @@ -3,9 +3,11 @@ using System.Collections.ObjectModel; using System.Linq; +using Moq; using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; +using Perspex.LogicalTree; using Xunit; namespace Perspex.Controls.UnitTests.Primitives @@ -13,114 +15,144 @@ namespace Perspex.Controls.UnitTests.Primitives public class TabStripTests { [Fact] - public void First_Tab_Should_Be_Selected_By_Default() + public void Header_Of_IHeadered_Items_Should_Be_Used() { + var items = new[] + { + Mock.Of(x => x.Header == "foo"), + Mock.Of(x => x.Header == "bar"), + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] - { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Items = items, }; target.ApplyTemplate(); - Assert.Equal(0, target.SelectedIndex); - Assert.Equal(target.Items.Cast().First(), target.SelectedItem); - Assert.Equal(target.Items.Cast().First(), target.SelectedTab); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new[] { "foo", "bar" }, result); } [Fact] - public void Setting_SelectedItem_Should_Set_SelectedTab() + public void Data_Of_Non_IHeadered_Items_Should_Be_Used() { + var items = new[] + { + "foo", + "bar" + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] - { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Items = items, }; target.ApplyTemplate(); - target.SelectedItem = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedTab, target.SelectedItem); + var result = target.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new[] { "foo", "bar" }, result); } [Fact] - public void Setting_SelectedTab_Should_Set_SelectedItem() + public void First_Tab_Should_Be_Selected_By_Default() { + var items = new[] + { + new TabItem + { + Name = "first" + }, + new TabItem + { + Name = "second" + }, + }; + var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = new[] + Items = items, + }; + + target.ApplyTemplate(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Same(items[0], target.SelectedItem); + } + + [Fact] + public void Setting_SelectedItem_Should_Set_Selection() + { + var items = new[] + { + new TabItem + { + Name = "first" + }, + new TabItem { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - } + Name = "second" + }, + }; + + var target = new TabStrip + { + Template = new FuncControlTemplate(CreateTabStripTemplate), + Items = items, + SelectedItem = items[1], }; target.ApplyTemplate(); - target.SelectedTab = target.Items.Cast().ElementAt(1); - Assert.Same(target.SelectedItem, target.SelectedTab); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); } [Fact] public void Removing_Selected_Should_Select_Next() { - var list = new ObservableCollection() + var items = new ObservableCollection() + { + new TabItem + { + Name = "first" + }, + new TabItem { - new TabItem - { - Name = "first" - }, - new TabItem - { - Name = "second" - }, - new TabItem - { - Name = "3rd" - }, - }; + Name = "second" + }, + new TabItem + { + Name = "3rd" + }, + }; var target = new TabStrip { Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = list + Items = items }; target.ApplyTemplate(); - target.SelectedTab = list[1]; - Assert.Same(list[1], target.SelectedTab); - list.RemoveAt(1); + target.SelectedItem = items[1]; + Assert.Same(items[1], target.SelectedItem); + items.RemoveAt(1); // Assert for former element [2] now [1] == "3rd" - Assert.Same(list[1], target.SelectedTab); - Assert.Same("3rd", target.SelectedTab.Name); + Assert.Equal(1, target.SelectedIndex); + Assert.Same(items[1], target.SelectedItem); + Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); } private Control CreateTabStripTemplate(TabStrip parent) @@ -128,7 +160,8 @@ namespace Perspex.Controls.UnitTests.Primitives return new ItemsPresenter { Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], + [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }; } } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs index d0f0547854..40d8e1c0c0 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -118,27 +118,6 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.Equal(target, child.GetLogicalParent()); } - [Fact] - public void Templated_Child_Should_Have_ApplyTemplate_Called_With_Logical_Then_Visual_Parent() - { - var target = new TemplatedControl - { - Template = new FuncControlTemplate(_ => new ApplyTemplateTracker()) - }; - - target.ApplyTemplate(); - - var child = (ApplyTemplateTracker)target.GetVisualChildren().Single(); - - Assert.Equal( - new[] - { - new Tuple(null, target), - new Tuple(target, target), - }, - child.Invocations); - } - [Fact] public void Nested_TemplatedControls_Should_Be_Expanded_And_Have_Correct_TemplatedParent() { @@ -186,6 +165,76 @@ namespace Perspex.Controls.UnitTests.Primitives templatedParents); } + + + [Fact] + public void Nested_TemplatedControls_Should_Register_With_Correct_NameScope() + { + var target = new ContentControl + { + Template = new FuncControlTemplate(ScrollingContentControlTemplate), + Content = "foo" + }; + + target.ApplyTemplate(); + + var border = target.GetVisualChildren().FirstOrDefault(); + Assert.IsType(border); + var scrollViewer = border.GetVisualChildren().FirstOrDefault(); + Assert.IsType(scrollViewer); + var scrollContentPresenter = scrollViewer.GetVisualChildren().FirstOrDefault(); + Assert.IsType(scrollContentPresenter); + var contentPresenter = scrollContentPresenter.GetVisualChildren().FirstOrDefault(); + Assert.IsType(contentPresenter); + + var borderNs = NameScope.GetNameScope((Control)border); + var scrollContentPresenterNs = NameScope.GetNameScope((Control)scrollContentPresenter); + Assert.NotNull(borderNs); + Assert.Same(scrollViewer, borderNs.Find("ScrollViewer")); + Assert.Same(contentPresenter, borderNs.Find("PART_ContentPresenter")); + Assert.Same(scrollContentPresenter, scrollContentPresenterNs.Find("PART_ContentPresenter")); + } + + [Fact] + public void ApplyTemplate_Should_Raise_TemplateApplied() + { + var target = new TestTemplatedControl + { + Template = new FuncControlTemplate(_ => new Decorator()) + }; + + var raised = false; + + target.TemplateApplied += (s, e) => + { + Assert.Equal(TemplatedControl.TemplateAppliedEvent, e.RoutedEvent); + Assert.Same(target, e.Source); + Assert.NotNull(e.NameScope); + raised = true; + }; + + target.ApplyTemplate(); + + Assert.True(raised); + } + + private static IControl ScrollingContentControlTemplate(ContentControl control) + { + return new Border + { + Child = new ScrollViewer + { + Template = new FuncControlTemplate(ScrollViewerTemplate), + Name = "ScrollViewer", + Content = new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = control[!ContentControl.ContentProperty], + } + } + }; + } + private static IControl ItemsControlTemplate(ItemsControl control) { return new Border @@ -195,9 +244,9 @@ namespace Perspex.Controls.UnitTests.Primitives Template = new FuncControlTemplate(ScrollViewerTemplate), Content = new ItemsPresenter { - Name = "itemsPresenter", - [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], - [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], + Name = "PART_ItemsPresenter", + [!ItemsPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], + [!ItemsPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], } } }; @@ -207,7 +256,7 @@ namespace Perspex.Controls.UnitTests.Primitives { var result = new ScrollContentPresenter { - Name = "contentPresenter", + Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], }; diff --git a/tests/Perspex.Controls.UnitTests/TabControlTests.cs b/tests/Perspex.Controls.UnitTests/TabControlTests.cs index 541c1c8cd8..0f3172e715 100644 --- a/tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -7,6 +7,7 @@ using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.LogicalTree; +using Perspex.Styling; using Xunit; namespace Perspex.Controls.UnitTests @@ -37,60 +38,34 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); + Assert.Equal(0, target.SelectedIndex); Assert.Equal(selected, target.SelectedItem); - Assert.Equal(selected, target.SelectedTab); } [Fact] - public void Setting_SelectedItem_Should_Set_SelectedTab() + public void Logical_Children_Should_Be_TabItems() { - var target = new TabControl + var items = new[] { - Template = new FuncControlTemplate(CreateTabControlTemplate), - Items = new[] + new TabItem { - new TabItem - { - Name = "first", - Content = "foo", - }, - new TabItem - { - Name = "second", - Content = "bar", - }, - } + Content = "foo" + }, + new TabItem + { + Content = "bar" + }, }; - target.ApplyTemplate(); - target.SelectedItem = target.Items.Cast().ElementAt(1); - - Assert.Same(target.SelectedTab, target.SelectedItem); - } - - [Fact] - public void Logical_Child_Should_Be_Selected_Tab_Content() - { var target = new TabControl { Template = new FuncControlTemplate(CreateTabControlTemplate), - Items = new[] - { - new TabItem - { - Content = "foo" - }, - new TabItem - { - Content = "bar" - }, - }, + Items = items, }; + Assert.Equal(items, target.GetLogicalChildren()); target.ApplyTemplate(); - - Assert.Equal(1, target.GetLogicalChildren().Count()); - Assert.Equal("foo", ((TextBlock)target.GetLogicalChildren().First()).Text); + Assert.Equal(items, target.GetLogicalChildren()); } [Fact] @@ -127,7 +102,60 @@ namespace Perspex.Controls.UnitTests // compare with former [2] now [1] == "3rd" Assert.Same(collection[1], target.SelectedItem); - Assert.Same(target.SelectedTab, target.SelectedItem); + } + + + [Fact] + public void TabItem_Templates_Should_Be_Set_Before_TabItem_ApplyTemplate() + { + var collection = new[] + { + new TabItem + { + Name = "first", + Content = "foo", + }, + new TabItem + { + Name = "second", + Content = "bar", + }, + new TabItem + { + Name = "3rd", + Content = "barf", + }, + }; + + var template = new FuncControlTemplate(x => new Decorator()); + + using (PerspexLocator.EnterScope()) + { + PerspexLocator.CurrentMutable.Bind().ToConstant(new Styler()); + + var root = new TestRoot + { + Styles = new Styles + { + new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(TemplatedControl.TemplateProperty, template) + } + } + }, + Child = new TabControl + { + Template = new FuncControlTemplate(CreateTabControlTemplate), + Items = collection, + } + }; + } + + Assert.Same(collection[0].Template, template); + Assert.Same(collection[1].Template, template); + Assert.Same(collection[2].Template, template); } [Fact] @@ -155,26 +183,62 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - var dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + var carousel = (Carousel)target.Pages; + + var dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal(items[0], dataContext); target.SelectedIndex = 1; - dataContext = ((Button)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((Button)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal(items[1], dataContext); target.SelectedIndex = 2; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Base", dataContext); target.SelectedIndex = 3; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Qux", dataContext); target.SelectedIndex = 4; - dataContext = ((TextBlock)target.GetLogicalChildren().Single()).DataContext; + dataContext = ((TextBlock)carousel.Presenter.Panel.GetLogicalChildren().Single()).DataContext; Assert.Equal("Base", dataContext); } + /// + /// Non-headered control items should result in TabStripItems with empty content. + /// + /// + /// If a TabStrip is created with non IHeadered controls as its items, don't try to + /// display the control in the TabStripItem: if the TabStrip is part of a TabControl + /// then *that* will also try to display the control, resulting in dual-parentage + /// breakage. + /// + [Fact] + public void Non_IHeadered_Control_Items_Should_Be_Ignored() + { + var items = new[] + { + new TextBlock { Text = "foo" }, + new TextBlock { Text = "bar" }, + }; + + var target = new TabControl + { + Template = new FuncControlTemplate(CreateTabControlTemplate), + Items = items, + }; + + target.ApplyTemplate(); + + var result = target.TabStrip.GetLogicalChildren() + .OfType() + .Select(x => x.Content) + .ToList(); + + Assert.Equal(new object[] { string.Empty, string.Empty }, result); + } + private Control CreateTabControlTemplate(TabControl parent) { return new StackPanel @@ -183,18 +247,19 @@ namespace Perspex.Controls.UnitTests { new TabStrip { - Name = "tabStrip", + Name = "PART_TabStrip", Template = new FuncControlTemplate(CreateTabStripTemplate), - [!ItemsControl.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!!TabStrip.SelectedTabProperty] = parent[!!TabControl.SelectedTabProperty] + MemberSelector = TabControl.HeaderSelector, + [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!!TabStrip.SelectedIndexProperty] = parent[!!TabControl.SelectedIndexProperty] }, new Carousel { - Name = "carousel", + Name = "PART_Content", Template = new FuncControlTemplate(CreateCarouselTemplate), - MemberSelector = parent.ContentSelector, - [!ItemsControl.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!SelectingItemsControl.SelectedItemProperty] = parent[!SelectingItemsControl.SelectedItemProperty], + MemberSelector = TabControl.ContentSelector, + [!Carousel.ItemsProperty] = parent[!TabControl.ItemsProperty], + [!Carousel.SelectedItemProperty] = parent[!TabControl.SelectedItemProperty], } } }; @@ -204,8 +269,9 @@ namespace Perspex.Controls.UnitTests { return new ItemsPresenter { - Name = "itemsPresenter", + Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + [!CarouselPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], }; } @@ -213,7 +279,7 @@ namespace Perspex.Controls.UnitTests { return new CarouselPresenter { - Name = "itemsPresenter", + Name = "PART_ItemsPresenter", [!CarouselPresenter.ItemsProperty] = control[!ItemsControl.ItemsProperty], [!CarouselPresenter.ItemsPanelProperty] = control[!ItemsControl.ItemsPanelProperty], [!CarouselPresenter.MemberSelectorProperty] = control[!ItemsControl.MemberSelectorProperty], diff --git a/tests/Perspex.Controls.UnitTests/TestRoot.cs b/tests/Perspex.Controls.UnitTests/TestRoot.cs index 10973f7c9c..ea72005719 100644 --- a/tests/Perspex.Controls.UnitTests/TestRoot.cs +++ b/tests/Perspex.Controls.UnitTests/TestRoot.cs @@ -6,10 +6,11 @@ using Moq; using Perspex.Layout; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Controls.UnitTests { - internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot + internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot { public Size ClientSize => new Size(100, 100); diff --git a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs index 8cbf1dca3f..56f2ca8b3b 100644 --- a/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Controls.UnitTests/TestTemplatedControl.cs @@ -9,13 +9,14 @@ namespace Perspex.Controls.UnitTests { public bool OnTemplateAppliedCalled { get; private set; } - public new void AddVisualChild(IVisual visual) + public void AddVisualChild(IVisual visual) { - base.AddVisualChild(visual); + VisualChildren.Add(visual); } - protected override void OnTemplateApplied(INameScope nameScope) + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { + base.OnTemplateApplied(e); OnTemplateAppliedCalled = true; } } diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index 2db62dd619..9af8a15f84 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using Perspex.Collections; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; using Perspex.Input; @@ -42,7 +43,7 @@ namespace Perspex.Controls.UnitTests target.ApplyTemplate(); - var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single(); + var container = (TreeViewItem)target.ItemContainerGenerator.Containers.Single().ContainerControl; var header = (TextBlock)container.Header; Assert.Equal("Root", header.Text); } @@ -58,15 +59,18 @@ namespace Perspex.Controls.UnitTests DataTemplates = CreateNodeDataTemplate(), }; - // For TreeViewItem to find its parent TreeView, OnAttachedToVisualTree needs - // to be called, which requires an IRenderRoot. - var visualRoot = new TestRoot(); - visualRoot.Child = target; + // For TreeViewItem to find its parent TreeView, OnAttachedToLogicalTree needs + // to be called, which requires an IStyleRoot. + var root = new TestRoot(); + root.Child = target; ApplyTemplates(target); - var container = target.ItemContainerGenerator.TreeContainerFromItem( + var container = target.ItemContainerGenerator.Index.ContainerFromItem( tree[0].Children[1].Children[0]); + + Assert.NotNull(container); + var header = ((TreeViewItem)container).Header; var headerContent = ((TextBlock)header).Text; @@ -89,7 +93,9 @@ namespace Perspex.Controls.UnitTests ApplyTemplates(target); var item = tree[0].Children[1].Children[0]; - var container = (TreeViewItem)target.ItemContainerGenerator.TreeContainerFromItem(item); + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item); + + Assert.NotNull(container); container.RaiseEvent(new PointerPressEventArgs { @@ -122,6 +128,28 @@ namespace Perspex.Controls.UnitTests Assert.Equal(new[] { "Foo", "Bar", "Baz " }, result); } + [Fact] + public void Removing_Item_Should_Remove_Itself_And_Children_From_Index() + { + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + DataTemplates = CreateNodeDataTemplate(), + Items = tree, + }; + + var root = new TestRoot(); + root.Child = target; + ApplyTemplates(target); + + Assert.Equal(4, target.ItemContainerGenerator.Index.Items.Count()); + + tree[0].Children.RemoveAt(1); + + Assert.Equal(2, target.ItemContainerGenerator.Index.Items.Count()); + } + [Fact] public void DataContexts_Should_Be_Correctly_Set() { @@ -174,12 +202,12 @@ namespace Perspex.Controls.UnitTests private IList CreateTestTreeData() { - return new[] + return new PerspexList { new Node { Value = "Root", - Children = new[] + Children = new PerspexList { new Node { @@ -188,7 +216,7 @@ namespace Perspex.Controls.UnitTests new Node { Value = "Child2", - Children = new[] + Children = new PerspexList { new Node { @@ -265,7 +293,7 @@ namespace Perspex.Controls.UnitTests private class Node { public string Value { get; set; } - public IList Children { get; set; } + public IPerspexList Children { get; set; } } } } diff --git a/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs b/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs index c26ecf0ecb..7946f96523 100644 --- a/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs +++ b/tests/Perspex.Interactivity.UnitTests/InteractiveTests.cs @@ -395,7 +395,7 @@ namespace Perspex.Interactivity.UnitTests set { - AddVisualChildren(value.Cast()); + VisualChildren.AddRange(value.Cast()); } } diff --git a/tests/Perspex.Layout.UnitTests/MeasureTests.cs b/tests/Perspex.Layout.UnitTests/MeasureTests.cs new file mode 100644 index 0000000000..345ecfe6d6 --- /dev/null +++ b/tests/Perspex.Layout.UnitTests/MeasureTests.cs @@ -0,0 +1,51 @@ +// 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 Perspex.Controls; +using Xunit; + +namespace Perspex.Layout.UnitTests +{ + public class MeasureTests + { + [Fact] + public void Negative_Margin_Larger_Than_Constraint_Should_Request_Width_0() + { + Control target; + + var outer = new Decorator + { + Width = 100, + Height = 100, + Child = target = new Control + { + Margin = new Thickness(-100, 0, 0, 0), + } + }; + + outer.Measure(Size.Infinity); + + Assert.Equal(0, target.DesiredSize.Width); + } + + [Fact] + public void Negative_Margin_Larger_Than_Constraint_Should_Request_Height_0() + { + Control target; + + var outer = new Decorator + { + Width = 100, + Height = 100, + Child = target = new Control + { + Margin = new Thickness(0, -100, 0, 0), + } + }; + + outer.Measure(Size.Infinity); + + Assert.Equal(0, target.DesiredSize.Height); + } + } +} diff --git a/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj b/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj index 08e0e2c7a0..c0139c54d2 100644 --- a/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj +++ b/tests/Perspex.Layout.UnitTests/Perspex.Layout.UnitTests.csproj @@ -86,6 +86,7 @@ + diff --git a/tests/Perspex.LeakTests/ControlTests.cs b/tests/Perspex.LeakTests/ControlTests.cs index a44532dc5c..78dc8d764e 100644 --- a/tests/Perspex.LeakTests/ControlTests.cs +++ b/tests/Perspex.LeakTests/ControlTests.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Linq; using JetBrains.dotMemoryUnit; using Perspex.Controls; +using Perspex.Controls.Primitives; using Perspex.Controls.Templates; +using Perspex.VisualTree; using Xunit; using Xunit.Abstractions; @@ -81,6 +83,172 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + [Fact] + public void Templated_Child_Is_Freed_When_Template_Cleared() + { + Func run = () => + { + var window = new Window + { + Content = new TestTemplatedControl() + }; + + // Do a layout and make sure that the control gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.IsType(window.Presenter.Child.GetVisualChildren().SingleOrDefault()); + + // Clear the template and ensure the control template gets removed + ((TestTemplatedControl)window.Content).Template = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count()); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void ScrollViewer_With_Content_Is_Freed() + { + Func run = () => + { + var window = new Window + { + Content = new ScrollViewer + { + Content = new Canvas() + } + }; + + // Do a layout and make sure that ScrollViewer gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.IsType(((ScrollViewer)window.Presenter.Child).Presenter.Child); + + // Clear the content and ensure the ScrollViewer is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void TextBox_Is_Freed() + { + Func run = () => + { + var window = new Window + { + Content = new TextBox() + }; + + // Do a layout and make sure that TextBox gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); + + // Clear the content and ensure the TextBox is removed. + window.Content = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void TextBox_With_Xaml_Binding_Is_Freed() + { + Func run = () => + { + var window = new Window + { + DataContext = new Node { Name = "foo" }, + Content = new TextBox() + }; + + var binding = new Perspex.Markup.Xaml.Data.Binding + { + Path = "Name" + }; + + binding.Bind((TextBox)window.Content, TextBox.TextProperty); + + // Do a layout and make sure that TextBox gets added to visual tree and its + // Text property set. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text); + + // Clear the content and DataContext and ensure the TextBox is removed. + window.Content = null; + window.DataContext = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Null(window.Presenter.Child); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + [Fact] + public void TextBox_ScrollViewer_Is_Freed_When_Template_Cleared() + { + Func run = () => + { + var window = new Window + { + Content = new TextBox() + }; + + // Do a layout and make sure that TextBox gets added to visual tree and its + // template applied. + window.LayoutManager.ExecuteLayoutPass(); + Assert.IsType(window.Presenter.Child); + Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count()); + + // Clear the template and ensure the TextBox template gets removed + ((TextBox)window.Content).Template = null; + window.LayoutManager.ExecuteLayoutPass(); + Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count()); + + return window; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + [Fact] public void TreeView_Is_Freed() { @@ -101,12 +269,12 @@ namespace Perspex.LeakTests Content = target = new TreeView { DataTemplates = new DataTemplates - { - new FuncTreeDataTemplate( - x => new TextBlock { Text = x.Name }, - x => x.Children, - x => true) - }, + { + new FuncTreeDataTemplate( + x => new TextBlock { Text = x.Name }, + x => x.Children, + x => true) + }, Items = nodes } }; @@ -129,6 +297,21 @@ namespace Perspex.LeakTests Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } + private class TestTemplatedControl : TemplatedControl + { + public static readonly PerspexProperty IsCanvasVisibleProperty = + PerspexProperty.Register("IsCanvasVisible"); + + public TestTemplatedControl() + { + Template = new FuncControlTemplate(parent => + new Canvas + { + [~IsVisibleProperty] = parent[~IsCanvasVisibleProperty] + }); + } + } + private class Node { public string Name { get; set; } diff --git a/tests/Perspex.LeakTests/TestApp.cs b/tests/Perspex.LeakTests/TestApp.cs index d0df7c18fc..8c08c57fc3 100644 --- a/tests/Perspex.LeakTests/TestApp.cs +++ b/tests/Perspex.LeakTests/TestApp.cs @@ -20,11 +20,15 @@ namespace Perspex.LeakTests var fixture = new Fixture().Customize(new AutoMoqCustomization()); var windowImpl = new Mock(); var renderInterface = fixture.Create(); + var threadingInterface = Mock.Of(x => + x.CurrentThreadIsLoopThread == true); PerspexLocator.CurrentMutable .Bind().ToConstant(new AssetLoader()) .Bind().ToConstant(new PclPlatformWrapper()) .Bind().ToConstant(renderInterface) + .Bind().ToConstant(threadingInterface) + .Bind().ToConstant(new Mock().Object) .Bind().ToConstant(new WindowingPlatformMock(() => windowImpl.Object)); Styles = new DefaultTheme(); diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs new file mode 100644 index 0000000000..cfbc4bc29f --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -0,0 +1,89 @@ +// 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.Reactive; +using System.Reactive.Subjects; +using Microsoft.Reactive.Testing; +using Perspex.Markup.Data; +using Xunit; + +namespace Perspex.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_Lifetime + { + [Fact(Skip = "Not working yet")] + public void Should_Complete_When_Source_Observable_Completes() + { + var source = new BehaviorSubject(1); + var target = new ExpressionObserver(source, "Foo"); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + source.OnCompleted(); + + Assert.True(completed); + } + + [Fact] + public void Should_Unsubscribe_From_Source_Observable() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable( + OnNext(1, new { Foo = "foo" })); + var target = new ExpressionObserver(source, "Foo"); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + scheduler.Start(); + } + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo" }, result); + Assert.Equal(1, source.Subscriptions.Count); + Assert.NotEqual(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + } + + [Fact] + public void Should_Unsubscribe_From_Update_Observable() + { + var scheduler = new TestScheduler(); + var update = scheduler.CreateColdObservable(); + var target = new ExpressionObserver(() => new { Foo = "foo" }, "Foo", update); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + scheduler.Start(); + } + + Assert.Equal(new[] { "foo" }, result); + Assert.Equal(1, update.Subscriptions.Count); + Assert.NotEqual(Subscription.Infinite, update.Subscriptions[0].Unsubscribe); + } + + [Fact] + public void Should_Set_Node_Target_To_Null_On_Unsubscribe() + { + var target = new ExpressionObserver(new { Foo = "foo" }, "Foo"); + var result = new List(); + + using (target.Subscribe(x => result.Add(x))) + using (target.Subscribe(_ => { })) + { + Assert.NotNull(target.Node.Target); + } + + Assert.Equal(new[] { "foo" }, result); + Assert.Null(target.Node.Target); + } + + private Recorded> OnNext(long time, object value) + { + return new Recorded>(time, Notification.CreateOnNext(value)); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index fd1583106c..fe9336ebe6 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Perspex.Markup.Data; using Xunit; @@ -193,13 +194,14 @@ namespace Perspex.Markup.UnitTests.Data public void Empty_Expression_Should_Track_Root() { var data = new Class1 { Foo = "foo" }; - var target = new ExpressionObserver(() => data.Foo, ""); + var update = new Subject(); + var target = new ExpressionObserver(() => data.Foo, "", update); var result = new List(); target.Subscribe(x => result.Add(x)); data.Foo = "bar"; - target.UpdateRoot(); + update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar" }, result); } @@ -286,14 +288,15 @@ namespace Perspex.Markup.UnitTests.Data var first = new Class1 { Foo = "foo" }; var second = new Class1 { Foo = "bar" }; var root = first; - var target = new ExpressionObserver(() => root, "Foo"); + var update = new Subject(); + var target = new ExpressionObserver(() => root, "Foo", update); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); root = second; - target.UpdateRoot(); + update.OnNext(Unit.Default); root = null; - target.UpdateRoot(); + update.OnNext(Unit.Default); Assert.Equal(new[] { "foo", "bar", PerspexProperty.UnsetValue }, result); @@ -355,7 +358,7 @@ namespace Perspex.Markup.UnitTests.Data { } - public Recorded> OnNext(long time, object value) + private Recorded> OnNext(long time, object value) { return new Recorded>(time, Notification.CreateOnNext(value)); } diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index ad9843a103..161e79f6f6 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -84,6 +84,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/TestRoot.cs b/tests/Perspex.Markup.UnitTests/TestRoot.cs index 3649493f0c..26ac0ed0da 100644 --- a/tests/Perspex.Markup.UnitTests/TestRoot.cs +++ b/tests/Perspex.Markup.UnitTests/TestRoot.cs @@ -5,10 +5,11 @@ using System; using Perspex.Controls; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Markup.UnitTests { - public class TestRoot : Decorator, IRenderRoot, INameScope + public class TestRoot : Decorator, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs index 9b6230df17..270b6d3d9b 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Converters/PerspexPropertyConverterTest.cs @@ -2,11 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive; using Moq; using OmniXaml; using OmniXaml.ObjectAssembler.Commands; using OmniXaml.TypeConversion; using OmniXaml.Typing; +using Perspex.Collections; +using Perspex.Controls; using Perspex.Markup.Xaml.Converters; using Perspex.Styling; using Xunit; @@ -74,7 +77,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Converters public static readonly PerspexProperty FooProperty = PerspexProperty.Register("Foo"); - public Classes Classes + public IPerspexReadOnlyList Classes { get { throw new NotImplementedException(); } } @@ -93,6 +96,8 @@ namespace Perspex.Markup.Xaml.UnitTests.Converters { get { throw new NotImplementedException(); } } + + IObservable IStyleable.StyleDetach { get; } } private class AttachedOwner diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs index bde7297d47..c47d53d0a3 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs @@ -10,7 +10,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Data public class BindingTests_ElementName { [Fact] - public void Should_Bind_To_Element() + public void Should_Bind_To_Element_Path() { TextBlock target; var root = new TestRoot @@ -44,7 +44,42 @@ namespace Perspex.Markup.Xaml.UnitTests.Data } [Fact] - public void Should_Bind_To_Later_Added_Element() + public void Should_Bind_To_Element() + { + TextBlock source; + ContentControl target; + + var root = new TestRoot + { + Child = new StackPanel + { + Children = new Controls.Controls + { + (source = new TextBlock + { + Name = "source", + Text = "foo", + }), + (target = new ContentControl + { + Name = "target", + }) + } + } + }; + + var binding = new Binding + { + ElementName = "source", + }; + + binding.Bind(target, ContentControl.ContentProperty); + + Assert.Same(source, target.Content); + } + + [Fact] + public void Should_Bind_To_Later_Added_Element_Path() { TextBlock target; StackPanel stackPanel; @@ -79,5 +114,43 @@ namespace Perspex.Markup.Xaml.UnitTests.Data Assert.Equal("foo", target.Text); } + + [Fact] + public void Should_Bind_To_Later_Added_Element() + { + ContentControl target; + StackPanel stackPanel; + + var root = new TestRoot + { + Child = stackPanel = new StackPanel + { + Children = new Controls.Controls + { + (target = new ContentControl + { + Name = "target", + }), + } + } + }; + + var binding = new Binding + { + ElementName = "source", + }; + + binding.Bind(target, ContentControl.ContentProperty); + + var source = new TextBlock + { + Name = "source", + Text = "foo", + }; + + stackPanel.Children.Add(source); + + Assert.Same(source, target.Content); + } } } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs index b9db948265..e172e2ccb7 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs @@ -5,10 +5,11 @@ using System; using Perspex.Controls; using Perspex.Platform; using Perspex.Rendering; +using Perspex.Styling; namespace Perspex.Markup.Xaml.UnitTests { - public class TestRoot : Decorator, IRenderRoot, INameScope + public class TestRoot : Decorator, IRenderRoot, INameScope, IStyleRoot { private readonly NameScope _nameScope = new NameScope(); diff --git a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj index d161b1264f..d1a6119151 100644 --- a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj +++ b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj @@ -40,6 +40,10 @@ 4 + + ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll + True + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll @@ -80,6 +84,7 @@ + diff --git a/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs new file mode 100644 index 0000000000..6eba2de185 --- /dev/null +++ b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs @@ -0,0 +1,183 @@ +// 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 Moq; +using Perspex.Controls; +using Perspex.Media; +using Perspex.Rendering; +using Xunit; + +namespace Perspex.SceneGraph.UnitTests +{ + public class RenderTests_Culling + { + [Fact] + public void In_Bounds_Control_Should_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 98, + [Canvas.TopProperty] = 98, + }) + } + }; + + Render(container); + + Assert.True(target.Rendered); + } + + [Fact] + public void Out_Of_Bounds_Control_Should_Not_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + ClipToBounds = true, + [Canvas.LeftProperty] = 110, + [Canvas.TopProperty] = 110, + }) + } + }; + + Render(container); + + Assert.False(target.Rendered); + } + + [Fact] + public void Out_Of_Bounds_Child_Control_Should_Not_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + new Canvas + { + Width = 100, + Height = 100, + [Canvas.LeftProperty] = 50, + [Canvas.TopProperty] = 50, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + ClipToBounds = true, + [Canvas.LeftProperty] = 50, + [Canvas.TopProperty] = 50, + }) + } + } + } + }; + + Render(container); + + Assert.False(target.Rendered); + } + + [Fact] + public void RenderTransform_Should_Be_Respected() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 110, + [Canvas.TopProperty] = 110, + RenderTransform = new TranslateTransform(-100, -100), + }) + } + }; + + Render(container); + + Assert.True(target.Rendered); + } + + [Fact] + public void Negative_Margin_Should_Be_Respected() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + new Border + { + Margin = new Thickness(100, 100, 0, 0), + Child = target = new TestControl + { + Width = 10, + Height = 10, + Margin = new Thickness(-100, -100, 0, 0), + } + } + } + }; + + Render(container); + + Assert.True(target.Rendered); + } + + private void Render(IControl control) + { + var ctx = CreateDrawingContext(); + control.Measure(Size.Infinity); + control.Arrange(new Rect(control.DesiredSize)); + ctx.Render(control); + } + + private DrawingContext CreateDrawingContext() + { + return new DrawingContext(Mock.Of()); + } + + private class TestControl : Control + { + public bool Rendered { get; private set; } + + public override void Render(DrawingContext context) + { + Rendered = true; + } + } + } +} diff --git a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs index 88f47d7876..21a4427fbc 100644 --- a/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs +++ b/tests/Perspex.SceneGraph.UnitTests/TestVisual.cs @@ -20,8 +20,6 @@ namespace Perspex.SceneGraph.UnitTests public class TestVisual : Visual { - public new PerspexObject InheritanceParent => base.InheritanceParent; - public IVisual Child { get @@ -33,34 +31,34 @@ namespace Perspex.SceneGraph.UnitTests { if (Child != null) { - RemoveVisualChild(Child); + VisualChildren.Remove(Child); } if (value != null) { - AddVisualChild(value); + VisualChildren.Add(value); } } } public void AddChild(Visual v) { - AddVisualChild(v); + VisualChildren.Add(v); } public void AddChildren(IEnumerable v) { - AddVisualChildren(v); + VisualChildren.AddRange(v); } public void RemoveChild(Visual v) { - RemoveVisualChild(v); + VisualChildren.Remove(v); } public void ClearChildren() { - ClearVisualChildren(); + VisualChildren.Clear(); } } } diff --git a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs index f4785b66c1..00d8c01d0d 100644 --- a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs +++ b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs @@ -23,17 +23,6 @@ namespace Perspex.SceneGraph.UnitTests Assert.Equal(target, child.GetVisualParent()); } - [Fact] - public void Added_Child_Should_Have_InheritanceParent_Set() - { - var target = new TestVisual(); - var child = new TestVisual(); - - target.AddChild(child); - - Assert.Equal(target, child.InheritanceParent); - } - [Fact] public void Added_Child_Should_Notify_VisualParent_Changed() { @@ -60,18 +49,6 @@ namespace Perspex.SceneGraph.UnitTests Assert.Null(child.GetVisualParent()); } - [Fact] - public void Removed_Child_Should_Have_InheritanceParent_Cleared() - { - var target = new TestVisual(); - var child = new TestVisual(); - - target.AddChild(child); - target.RemoveChild(child); - - Assert.Null(child.InheritanceParent); - } - [Fact] public void Clearing_Children_Should_Clear_VisualParent() { diff --git a/tests/Perspex.SceneGraph.UnitTests/packages.config b/tests/Perspex.SceneGraph.UnitTests/packages.config index 8cbc4ec26f..3cf2b984d5 100644 --- a/tests/Perspex.SceneGraph.UnitTests/packages.config +++ b/tests/Perspex.SceneGraph.UnitTests/packages.config @@ -1,5 +1,6 @@  + diff --git a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj index 4099319b5a..cc22e7f257 100644 --- a/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj +++ b/tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj @@ -40,6 +40,11 @@ 4 + + ..\..\packages\Rx-Testing.2.2.5\lib\net45\Microsoft.Reactive.Testing.dll + True + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll @@ -85,10 +90,12 @@ + + diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 0df2e061a3..8a579223b3 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -2,11 +2,14 @@ // 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.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; +using Perspex.Controls; using Perspex.Styling; using Xunit; @@ -43,7 +46,7 @@ namespace Perspex.Styling.UnitTests } [Fact] - public async Task Child_Matches_Control_When_It_Is_Child_OfType_And_Class() + public async void Child_Matches_Control_When_It_Is_Child_OfType_And_Class() { var parent = new TestLogical1(); var child = new TestLogical2(); @@ -52,6 +55,7 @@ namespace Perspex.Styling.UnitTests var selector = new Selector().OfType().Class("foo").Child().OfType(); var activator = selector.Match(child).ObservableResult; + var result = new List(); Assert.False(await activator.Take(1)); parent.Classes.Add("foo"); @@ -80,6 +84,8 @@ namespace Perspex.Styling.UnitTests public string Name { get; set; } + public bool IsAttachedToLogicalTree { get; } + public IPerspexReadOnlyList LogicalChildren { get; set; } public ILogical LogicalParent { get; set; } @@ -88,6 +94,8 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + IObservable IStyleable.StyleDetach { get; } + public IPropertyBag InheritanceParent { get @@ -96,6 +104,8 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs index 9d98bd9a12..3232279e5e 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Class.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using Moq; +using Perspex.Controls; using Perspex.Styling; using Xunit; diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 7b5f78e6bb..f7ce2beaa9 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -3,10 +3,12 @@ using System; using System.Linq; +using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; +using Perspex.Controls; using Perspex.Styling; using Xunit; @@ -112,6 +114,8 @@ namespace Perspex.Styling.UnitTests public string Name { get; set; } + public bool IsAttachedToLogicalTree { get; } + public IPerspexReadOnlyList LogicalChildren { get; set; } public ILogical LogicalParent { get; set; } @@ -128,6 +132,10 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + + IObservable IStyleable.StyleDetach { get; } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs index 0f14820f59..89cd535338 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Multiple.cs @@ -44,7 +44,7 @@ namespace Perspex.Styling.UnitTests activator.Subscribe(x => values.Add(x)); Assert.Equal(new[] { false }, values); - control.Classes.Add("foo", "bar"); + control.Classes.AddRange(new[] { "foo", "bar" }); Assert.Equal(new[] { false, true }, values); control.Classes.Remove("foo"); Assert.Equal(new[] { false, true, false }, values); diff --git a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs index 2172bc39f7..9669904af6 100644 --- a/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleActivatorTests.cs @@ -2,18 +2,49 @@ // 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.Reactive; using System.Reactive.Linq; +using Microsoft.Reactive.Testing; using Xunit; namespace Perspex.Styling.UnitTests { - public class StyleActivatorTests + public class StyleActivatorTests : ReactiveTest { + [Fact] + public void Activator_Should_Subscribe_To_Inputs_On_First_Subscription() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = StyleActivator.And(new[] { source }); + + Assert.Equal(0, source.Subscriptions.Count); + target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + } + + [Fact] + public void Activator_Should_Unsubscribe_From_Inputs_After_Last_Subscriber_Completes() + { + var scheduler = new TestScheduler(); + var source = scheduler.CreateColdObservable(); + var target = StyleActivator.And(new[] { source }); + + var dispose = target.Subscribe(_ => { }); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(Subscription.Infinite, source.Subscriptions[0].Unsubscribe); + + dispose.Dispose(); + Assert.Equal(1, source.Subscriptions.Count); + Assert.Equal(0, source.Subscriptions[0].Unsubscribe); + } + [Fact] public void Activator_And_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -37,7 +68,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -54,7 +85,7 @@ namespace Perspex.Styling.UnitTests } [Fact] - public void Activator_And_Should_Unsubscribe_All_When_Input_Completes_On_False() + public void Activator_And_Should_Complete_When_Input_Completes_On_False() { var inputs = new[] { @@ -62,50 +93,22 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.And); + var target = StyleActivator.And(inputs); var result = new TestObserver(); + var completed = false; - target.Subscribe(result); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - inputs[1].OnNext(true); - Assert.True(result.GetValue()); + target.Subscribe(_ => { }, () => completed = true); inputs[0].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - - [Fact] - public void Activator_And_Should_Not_Unsubscribe_All_When_Input_Completes_On_True() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.And); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); inputs[0].OnCompleted(); - Assert.Equal(1, inputs[0].SubscriberCount); - Assert.Equal(1, inputs[1].SubscriberCount); - Assert.Equal(1, inputs[2].SubscriberCount); + Assert.True(completed); } [Fact] public void Activator_Or_Should_Follow_Single_Input() { var inputs = new[] { new TestSubject(false) }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -129,7 +132,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -144,31 +147,6 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[2].SubscriberCount); } - [Fact] - public void Activator_Or_Should_Unsubscribe_All_When_Input_Completes_On_True() - { - var inputs = new[] - { - new TestSubject(false), - new TestSubject(false), - new TestSubject(true), - }; - var target = new StyleActivator(inputs, ActivatorMode.Or); - var result = new TestObserver(); - - target.Subscribe(result); - Assert.True(result.GetValue()); - inputs[2].OnNext(false); - Assert.False(result.GetValue()); - inputs[0].OnNext(true); - Assert.True(result.GetValue()); - inputs[0].OnCompleted(); - - Assert.Equal(0, inputs[0].SubscriberCount); - Assert.Equal(0, inputs[1].SubscriberCount); - Assert.Equal(0, inputs[2].SubscriberCount); - } - [Fact] public void Activator_Or_Should_Not_Unsubscribe_All_When_Input_Completes_On_False() { @@ -178,7 +156,7 @@ namespace Perspex.Styling.UnitTests new TestSubject(false), new TestSubject(true), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var result = new TestObserver(); target.Subscribe(result); @@ -189,7 +167,7 @@ namespace Perspex.Styling.UnitTests Assert.Equal(1, inputs[0].SubscriberCount); Assert.Equal(1, inputs[1].SubscriberCount); - Assert.Equal(1, inputs[2].SubscriberCount); + Assert.Equal(0, inputs[2].SubscriberCount); } [Fact] @@ -200,12 +178,25 @@ namespace Perspex.Styling.UnitTests Observable.Return(false), }; - var target = new StyleActivator(inputs, ActivatorMode.Or); + var target = StyleActivator.Or(inputs); var completed = false; target.Subscribe(_ => { }, () => completed = true); Assert.True(completed); } + + private Recorded>[] OnNextValues(params bool[] values) + { + var result = new List>>(); + var time = 1; + + foreach (var value in values) + { + result.Add(new Recorded>(time, Notification.CreateOnNext(value))); + } + + return result.ToArray(); + } } } diff --git a/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs b/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs new file mode 100644 index 0000000000..82a7f66d07 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/StyleBindingTests.cs @@ -0,0 +1,79 @@ +// 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.Reactive.Linq; +using System.Reactive.Subjects; +using Xunit; + +namespace Perspex.Styling.UnitTests +{ + public class StyleBindingTests + { + [Fact] + public async void Should_Produce_UnsetValue_On_Activator_False() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Produce_Value_On_Activator_True() + { + var activator = new BehaviorSubject(true); + var target = new StyleBinding(activator, 1, string.Empty); + var result = await target.Take(1); + + Assert.Equal(1, result); + } + + [Fact] + public void Should_Change_Value_On_Activator_Change() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + activator.OnNext(false); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result); + } + + [Fact] + public void Should_Change_Value_With_Source_Observable() + { + var activator = new BehaviorSubject(false); + var source = new BehaviorSubject(1); + var target = new StyleBinding(activator, source, string.Empty); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + activator.OnNext(true); + source.OnNext(2); + activator.OnNext(false); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, 2, PerspexProperty.UnsetValue }, result); + } + + [Fact] + public void Should_Complete_When_Activator_Completes() + { + var activator = new BehaviorSubject(false); + var target = new StyleBinding(activator, 1, string.Empty); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + activator.OnCompleted(); + + Assert.True(completed); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/StyleTests.cs b/tests/Perspex.Styling.UnitTests/StyleTests.cs index cda975b597..f915f4169c 100644 --- a/tests/Perspex.Styling.UnitTests/StyleTests.cs +++ b/tests/Perspex.Styling.UnitTests/StyleTests.cs @@ -166,7 +166,7 @@ namespace Perspex.Styling.UnitTests { var source = new BehaviorSubject("Foo"); - Style style = new Style(x => x.OfType().Class("foo")) + var style = new Style(x => x.OfType().Class("foo")) { Setters = new[] { @@ -187,6 +187,31 @@ namespace Perspex.Styling.UnitTests Assert.Equal("foodefault", target.Foo); } + [Fact] + public void Style_Should_Detach_When_Removed_From_Logical_Tree() + { + Border border; + + var style = new Style(x => x.OfType()) + { + Setters = new[] + { + new Setter(Border.BorderThicknessProperty, 4), + } + }; + + var root = new TestRoot + { + Child = border = new Border(), + }; + + style.Attach(border, null); + + Assert.Equal(4, border.BorderThickness); + root.Child = null; + Assert.Equal(0, border.BorderThickness); + } + private class Class1 : Control { public static readonly PerspexProperty FooProperty = diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index ad47f1eb27..82ab942a09 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -2,7 +2,10 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive; using System.Reactive.Subjects; +using Perspex.Collections; +using Perspex.Controls; namespace Perspex.Styling.UnitTests { @@ -36,6 +39,10 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + + IObservable IStyleable.StyleDetach { get; } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/TestRoot.cs b/tests/Perspex.Styling.UnitTests/TestRoot.cs new file mode 100644 index 0000000000..3223a033c6 --- /dev/null +++ b/tests/Perspex.Styling.UnitTests/TestRoot.cs @@ -0,0 +1,34 @@ +// 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 Moq; +using Perspex.Controls; +using Perspex.Layout; +using Perspex.Platform; +using Perspex.Rendering; + +namespace Perspex.Styling.UnitTests +{ + internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot, IStyleRoot + { + public Size ClientSize => new Size(100, 100); + + public ILayoutManager LayoutManager => new Mock().Object; + + public IRenderTarget RenderTarget + { + get { throw new NotImplementedException(); } + } + + public IRenderQueueManager RenderQueueManager + { + get { throw new NotImplementedException(); } + } + + public Point TranslatePointToScreen(Point p) + { + return new Point(); + } + } +} diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index 764c402138..1348b1d662 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -3,7 +3,10 @@ using System; using System.Collections.Generic; +using System.Reactive; using System.Reactive.Subjects; +using Perspex.Collections; +using Perspex.Controls; namespace Perspex.Styling.UnitTests { @@ -42,6 +45,10 @@ namespace Perspex.Styling.UnitTests } } + IPerspexReadOnlyList IStyleable.Classes => Classes; + + IObservable IStyleable.StyleDetach { get; } + public IObservable GetObservable(PerspexProperty property) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/packages.config b/tests/Perspex.Styling.UnitTests/packages.config index 3cf2b984d5..bacf5a37aa 100644 --- a/tests/Perspex.Styling.UnitTests/packages.config +++ b/tests/Perspex.Styling.UnitTests/packages.config @@ -6,6 +6,7 @@ +