diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index a8c3abe8f2..04ab17c4e1 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,7 +1,6 @@  - 1000 - True + 3000 True \ No newline at end of file diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index efcbb57244..8b93cf2fb1 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -225,6 +225,19 @@ namespace Avalonia return (T)GetValue((AvaloniaProperty)property); } + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + public bool IsAnimating(AvaloniaProperty property) + { + Contract.Requires(property != null); + VerifyAccess(); + + return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + } + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index a3c3015a38..41c2ad6e54 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -350,14 +350,15 @@ namespace Avalonia.Collections public void MoveRange(int oldIndex, int count, int newIndex) { var items = _inner.GetRange(oldIndex, count); + var modifiedNewIndex = newIndex; _inner.RemoveRange(oldIndex, count); if (newIndex > oldIndex) { - newIndex -= count; + modifiedNewIndex -= count; } - _inner.InsertRange(newIndex, items); + _inner.InsertRange(modifiedNewIndex, items); if (_collectionChanged != null) { diff --git a/src/Avalonia.Base/Collections/IAvaloniaList.cs b/src/Avalonia.Base/Collections/IAvaloniaList.cs index 0233cee7a9..48c36976a5 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaList.cs @@ -36,6 +36,21 @@ namespace Avalonia.Collections /// The items. void InsertRange(int index, IEnumerable items); + /// + /// Moves an item to a new index. + /// + /// The index of the item to move. + /// The index to move the item to. + void Move(int oldIndex, int newIndex); + + /// + /// Moves multiple items to a new index. + /// + /// The first index of the items to move. + /// The number of items to move. + /// The index to move the items to. + void MoveRange(int oldIndex, int count, int newIndex); + /// /// Removes multiple items from the collection. /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11bab2236..c11f8ada7e 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -31,6 +31,13 @@ namespace Avalonia /// The value. T GetValue(AvaloniaProperty property); + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + bool IsAnimating(AvaloniaProperty property); + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 3726fb7ae5..12a9e20528 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -52,6 +52,18 @@ namespace Avalonia _validate = validate; } + /// + /// Gets a value indicating whether the property is animating. + /// + public bool IsAnimating + { + get + { + return ValuePriority <= (int)BindingPriority.Animation && + GetLevel(ValuePriority).ActiveBindingIndex != -1; + } + } + /// /// Gets the owner of the value. /// diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 8c79e5dce5..59281c5ad0 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -549,7 +549,7 @@ namespace Avalonia.Controls } else { - if (addedDate.HasValue && !(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value)) + if (!(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value)) { foreach (DateTime item in SelectedDates) { diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index d4777b2f8a..6e1e1a05f1 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -621,7 +621,6 @@ namespace Avalonia.Controls Contract.Requires(property != null); Contract.Requires(selector != null); Contract.Requires(className != null); - Contract.Requires(property != null); if (string.IsNullOrWhiteSpace(className)) { diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index fa2e0c1e16..6b27c479ba 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -96,6 +96,16 @@ namespace Avalonia.Controls this.UpdateSelectionBoxItem(this.SelectedItem); } + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + + if (!e.Handled && e.NavigationMethod == NavigationMethod.Directional) + { + e.Handled = UpdateSelectionFromEventSource(e.Source); + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -104,7 +114,7 @@ namespace Avalonia.Controls if (!e.Handled) { if (e.Key == Key.F4 || - (e.Key == Key.Down && ((e.Modifiers & InputModifiers.Alt) != 0))) + ((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0))) { IsDropDownOpen = !IsDropDownOpen; e.Handled = true; @@ -114,6 +124,27 @@ namespace Avalonia.Controls IsDropDownOpen = false; e.Handled = true; } + + if (!IsDropDownOpen) + { + if (e.Key == Key.Down) + { + if (SelectedIndex == -1) + SelectedIndex = 0; + + if (++SelectedIndex >= ItemCount) + SelectedIndex = 0; + + e.Handled = true; + } + else if (e.Key == Key.Up) + { + if (--SelectedIndex < 0) + SelectedIndex = ItemCount - 1; + + e.Handled = true; + } + } } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index aa209e0462..4366de1cd6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Input; using Avalonia.LogicalTree; using Avalonia.Metadata; @@ -106,6 +107,12 @@ namespace Avalonia.Controls set { SetAndRaise(ItemsProperty, ref _items, value); } } + public int ItemCount + { + get; + private set; + } + /// /// Gets or sets the panel used to display the items. /// @@ -352,6 +359,10 @@ namespace Avalonia.Controls RemoveControlItemsFromLogicalChildren(e.OldItems); break; } + + int? count = (Items as IList)?.Count; + if (count != null) + ItemCount = (int)count; var collection = sender as ICollection; PseudoClasses.Set(":empty", collection == null || collection.Count == 0); diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 3272d3779b..6448f11491 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -115,6 +115,11 @@ namespace Avalonia.Controls VisualChildren.AddRange(e.NewItems.OfType()); break; + case NotifyCollectionChangedAction.Move: + LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); + VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex); + break; + case NotifyCollectionChangedAction.Remove: controls = e.OldItems.OfType().ToList(); LogicalChildren.RemoveAll(controls); @@ -132,11 +137,7 @@ namespace Avalonia.Controls break; case NotifyCollectionChangedAction.Reset: - controls = e.OldItems.OfType().ToList(); - LogicalChildren.Clear(); - VisualChildren.Clear(); - VisualChildren.AddRange(_children); - break; + throw new NotSupportedException(); } InvalidateMeasure(); diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ab09a4701d..563d394919 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -297,7 +297,7 @@ namespace Avalonia.Controls.Primitives .OfType() .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); - return item as IControl; + return item; } /// diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index de02c10764..8cf6b149cb 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -28,9 +28,6 @@ namespace Avalonia.Controls { ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Left); - VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Top); - IsIndeterminateProperty.Changed.AddClassHandler( (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); OrientationProperty.Changed.AddClassHandler( diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 079e571d29..fa3ecdedef 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -176,10 +176,7 @@ namespace Avalonia.Controls SelectedItem = item; - if (SelectedItem != null) - { - MarkContainerSelected(container, true); - } + MarkContainerSelected(container, true); } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 834f6d218b..409dd231ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -134,12 +134,14 @@ namespace Avalonia.Controls protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from) { + if (from == null) + return null; + var logicalScrollable = Parent as ILogicalScrollable; - var fromControl = from as IControl; - if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null) + if (logicalScrollable?.IsLogicalScrollEnabled == true) { - return logicalScrollable.GetControlInDirection(direction, fromControl); + return logicalScrollable.GetControlInDirection(direction, from); } else { diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs index 7e4e5a8564..d445f1cd70 100644 --- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs +++ b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs @@ -27,6 +27,12 @@ namespace Avalonia.Diagnostics.Views if (layer != null) { + if (_adorner != null) + { + ((Panel)_adorner.Parent).Children.Remove(_adorner); + _adorner = null; + } + _adorner = new Rectangle { Fill = new SolidColorBrush(0x80a0c5e8), diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index fbc189546c..9e4a3cbeae 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -320,7 +320,7 @@ namespace Avalonia.Media if (c == 'E') { readSign = false; - readExponent = c == 'E'; + readExponent = true; } } else diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f7befa646a..041d8f8f6b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -25,11 +25,9 @@ namespace Avalonia.Rendering private readonly IRenderLoop _renderLoop; private readonly IVisual _root; private readonly ISceneBuilder _sceneBuilder; - private readonly RenderLayers _layers; private bool _running; private Scene _scene; - private IRenderTarget _renderTarget; private DirtyVisuals _dirty; private IRenderTargetBitmapImpl _overlay; private bool _updateQueued; @@ -56,7 +54,7 @@ namespace Avalonia.Rendering _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); _renderLoop = renderLoop; } @@ -78,9 +76,9 @@ namespace Avalonia.Rendering Contract.Requires(renderTarget != null); _root = root; - _renderTarget = renderTarget; + RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); } /// @@ -94,6 +92,16 @@ namespace Avalonia.Rendering /// public string DebugFramesPath { get; set; } + /// + /// Gets the render layers. + /// + internal RenderLayers Layers { get; } + + /// + /// Gets the current render target. + /// + internal IRenderTarget RenderTarget { get; private set; } + /// public void AddDirty(IVisual visual) { @@ -173,9 +181,9 @@ namespace Avalonia.Rendering bool renderOverlay = DrawDirtyRects || DrawFps; bool composite = false; - if (_renderTarget == null) + if (RenderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); } if (renderOverlay) @@ -191,8 +199,8 @@ namespace Avalonia.Rendering if (scene.Generation != _lastSceneId) { - context = _renderTarget.CreateDrawingContext(this); - _layers.Update(scene, context); + context = RenderTarget.CreateDrawingContext(this); + Layers.Update(scene, context); RenderToLayers(scene); @@ -208,13 +216,13 @@ namespace Avalonia.Rendering if (renderOverlay) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderOverlay(scene, context); RenderComposite(scene, context); } else if (composite) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderComposite(scene, context); } @@ -224,8 +232,8 @@ namespace Avalonia.Rendering catch (RenderTargetCorruptedException ex) { Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget?.Dispose(); - _renderTarget = null; + RenderTarget?.Dispose(); + RenderTarget = null; } } @@ -235,9 +243,11 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty) + if (!clipBounds.IsEmpty && node.Opacity > 0) { - node.BeginRender(context); + var isLayerRoot = node.Visual == layer; + + node.BeginRender(context, isLayerRoot); foreach (var operation in node.DrawOperations) { @@ -251,7 +261,7 @@ namespace Avalonia.Rendering Render(context, (VisualNode)child, layer, clipBounds); } - node.EndRender(context); + node.EndRender(context, isLayerRoot); } } } @@ -262,7 +272,7 @@ namespace Avalonia.Rendering { foreach (var layer in scene.Layers) { - var renderTarget = _layers[layer.LayerRoot].Bitmap; + var renderTarget = Layers[layer.LayerRoot].Bitmap; var node = (VisualNode)scene.FindNode(layer.LayerRoot); if (node != null) @@ -322,7 +332,7 @@ namespace Avalonia.Rendering foreach (var layer in scene.Layers) { - var bitmap = _layers[layer.LayerRoot].Bitmap; + var bitmap = Layers[layer.LayerRoot].Bitmap; var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); if (layer.GeometryClip != null) @@ -353,7 +363,7 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context, clientRect, true); + RenderFps(context, clientRect, scene.Layers.Count); } } @@ -442,7 +452,7 @@ namespace Avalonia.Rendering { var index = 0; - foreach (var layer in _layers) + foreach (var layer in Layers) { var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png"); layer.Bitmap.Save(fileName); diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 2d5a864089..84313f0906 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -69,7 +69,7 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context.PlatformImpl, _root.Bounds, true); + RenderFps(context.PlatformImpl, _root.Bounds, null); } } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 707b31998a..eac362e997 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -22,15 +22,12 @@ namespace Avalonia.Rendering }; } - protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount) + protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (incrementFrameCount) - { - ++_framesThisSecond; - } + ++_framesThisSecond; if (elapsed.TotalSeconds > 1) { @@ -39,7 +36,15 @@ namespace Avalonia.Rendering _lastFpsUpdate = now; } - _fpsText.Text = string.Format("FPS: {0:000}", _fps); + if (layerCount.HasValue) + { + _fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps); + } + else + { + _fpsText.Text = string.Format("FPS: {0:000}", _fps); + } + var size = _fpsText.Measure(); var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 0d2fc17b95..234cadbf31 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -72,13 +72,15 @@ namespace Avalonia.Rendering.SceneGraph /// Sets up the drawing context for rendering the node's geometry. /// /// The drawing context. - void BeginRender(IDrawingContextImpl context); + /// Whether to skip pushing the control's opacity. + void BeginRender(IDrawingContextImpl context, bool skipOpacity); /// /// Resets the drawing context after rendering the node's geometry. /// /// The drawing context. - void EndRender(IDrawingContextImpl context); + /// Whether to skip popping the control's opacity. + void EndRender(IDrawingContextImpl context, bool skipOpacity); /// /// Hit test the geometry in this node. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 90ef78de37..8f4f487e08 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -167,7 +167,6 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - var startLayer = opacity < 1 || visual.OpacityMask != null; var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); forceRecurse = forceRecurse || @@ -179,9 +178,11 @@ namespace Avalonia.Rendering.SceneGraph node.ClipToBounds = clipToBounds; node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; - node.OpacityMask = visual.OpacityMask; - if (startLayer) + // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. + node.OpacityMask = visual.OpacityMask?.ToImmutable(); + + if (ShouldStartLayer(visual)) { if (node.LayerRoot != visual) { @@ -192,7 +193,7 @@ namespace Avalonia.Rendering.SceneGraph UpdateLayer(node, scene.Layers[node.LayerRoot]); } } - else if (!startLayer && node.LayerRoot == node.Visual && node.Parent != null) + else if (node.LayerRoot == node.Visual && node.Parent != null) { ClearLayer(scene, node); } @@ -366,6 +367,14 @@ namespace Avalonia.Rendering.SceneGraph } } + private static bool ShouldStartLayer(IVisual visual) + { + var o = visual as IAvaloniaObject; + return visual.VisualChildren.Count > 0 && + o != null && + o.IsAnimating(Visual.OpacityProperty); + } + private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { IGeometryImpl result = null; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index dd5740e4a9..6bea4d9bd6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph private List _children; private List _drawOperations; private bool _drawOperationsCloned; + private Matrix transformRestore; /// /// Initializes a new instance of the class. @@ -218,8 +219,10 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void BeginRender(IDrawingContextImpl context) + public void BeginRender(IDrawingContextImpl context, bool skipOpacity) { + transformRestore = context.Transform; + if (ClipToBounds) { context.Transform = Matrix.Identity; @@ -228,24 +231,47 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = Transform; + if (Opacity != 1 && !skipOpacity) + { + context.PushOpacity(Opacity); + } + if (GeometryClip != null) { context.PushGeometryClip(GeometryClip); } + + if (OpacityMask != null) + { + context.PushOpacityMask(OpacityMask, ClipBounds); + } } /// - public void EndRender(IDrawingContextImpl context) + public void EndRender(IDrawingContextImpl context, bool skipOpacity) { + if (OpacityMask != null) + { + context.PopOpacityMask(); + } + if (GeometryClip != null) { context.PopGeometryClip(); } + if (Opacity != 1 && !skipOpacity) + { + context.PopOpacity(); + } + if (ClipToBounds) { + context.Transform = Matrix.Identity; context.PopClip(); } + + context.Transform = transformRestore; } private Rect CalculateBounds() diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bc65d4f69f..3662fe50be 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/src/Avalonia.Visuals/Visual.cs @@ -537,6 +537,19 @@ namespace Avalonia v.SetVisualParent(null); } + break; + + case NotifyCollectionChangedAction.Replace: + foreach (Visual v in e.OldItems) + { + v.SetVisualParent(null); + } + + foreach (Visual v in e.NewItems) + { + v.SetVisualParent(this); + } + break; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index eb5f87ea2a..7f750144df 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -66,7 +66,7 @@ namespace Avalonia.Markup.Xaml.Data /// /// Gets or sets the binding path. /// - public string Path { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets the binding priority. @@ -93,53 +93,53 @@ namespace Avalonia.Markup.Xaml.Data bool enableDataValidation = false) { Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - - var pathInfo = ParsePath(Path); - ValidateState(pathInfo); + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - + ExpressionObserver observer; - if (pathInfo.ElementName != null || ElementName != null) + if (ElementName != null) { observer = CreateElementObserver( (target as IControl) ?? (anchor as IControl), - pathInfo.ElementName ?? ElementName, - pathInfo.Path); + ElementName, + Path, + enableDataValidation); } else if (Source != null) { - observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation); + observer = CreateSourceObserver(Source, Path, enableDataValidation); } else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { observer = CreateDataContexObserver( target, - pathInfo.Path, + Path, targetProperty == Control.DataContextProperty, anchor, enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation); + observer = CreateSourceObserver(target, Path, enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver(target, pathInfo.Path); + observer = CreateTemplatedParentObserver(target, Path, enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { - if (RelativeSource.AncestorType == null) + if (RelativeSource.Tree == TreeType.Visual && RelativeSource.AncestorType == null) { - throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor."); + throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } observer = CreateFindAncestorObserver( (target as IControl) ?? (anchor as IControl), - pathInfo.Path); + RelativeSource, + Path, + enableDataValidation); } else { @@ -168,53 +168,6 @@ namespace Avalonia.Markup.Xaml.Data return new InstancedBinding(subject, Mode, Priority); } - private static PathInfo ParsePath(string path) - { - var result = new PathInfo(); - - if (string.IsNullOrWhiteSpace(path) || path == ".") - { - result.Path = string.Empty; - } - else if (path.StartsWith("#")) - { - var dot = path.IndexOf('.'); - - if (dot != -1) - { - result.Path = path.Substring(dot + 1); - result.ElementName = path.Substring(1, dot - 1); - } - else - { - result.Path = string.Empty; - result.ElementName = path.Substring(1); - } - } - else - { - result.Path = path; - } - - return result; - } - - private void ValidateState(PathInfo pathInfo) - { - if (pathInfo.ElementName != null && ElementName != null) - { - throw new InvalidOperationException( - "ElementName property cannot be set when an #elementName path is provided."); - } - - if ((pathInfo.ElementName != null || ElementName != null) && - RelativeSource != null) - { - throw new InvalidOperationException( - "ElementName property cannot be set with a RelativeSource."); - } - } - private ExpressionObserver CreateDataContexObserver( IAvaloniaObject target, string path, @@ -256,7 +209,11 @@ namespace Avalonia.Markup.Xaml.Data } } - private ExpressionObserver CreateElementObserver(IControl target, string elementName, string path) + private ExpressionObserver CreateElementObserver( + IControl target, + string elementName, + string path, + bool enableDataValidation) { Contract.Requires(target != null); @@ -264,35 +221,39 @@ namespace Avalonia.Markup.Xaml.Data var result = new ExpressionObserver( ControlLocator.Track(target, elementName), path, - false, + enableDataValidation, description); return result; } private ExpressionObserver CreateFindAncestorObserver( IControl target, - string path) + RelativeSource relativeSource, + string path, + bool enableDataValidation) { Contract.Requires(target != null); return new ExpressionObserver( - ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1), - path); + ControlLocator.Track(target, relativeSource.Tree, relativeSource.AncestorLevel - 1, relativeSource.AncestorType), + path, + enableDataValidation); } private ExpressionObserver CreateSourceObserver( object source, string path, - bool enabledDataValidation) + bool enableDataValidation) { Contract.Requires(source != null); - return new ExpressionObserver(source, path, enabledDataValidation); + return new ExpressionObserver(source, path, enableDataValidation); } private ExpressionObserver CreateTemplatedParentObserver( IAvaloniaObject target, - string path) + string path, + bool enableDataValidation) { Contract.Requires(target != null); @@ -303,7 +264,8 @@ namespace Avalonia.Markup.Xaml.Data var result = new ExpressionObserver( () => target.GetValue(Control.TemplatedParentProperty), path, - update); + update, + enableDataValidation); return result; } @@ -328,6 +290,7 @@ namespace Avalonia.Markup.Xaml.Data { public string Path { get; set; } public string ElementName { get; set; } + public RelativeSource RelativeSource { get; set; } } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs index f77df6853b..825d3b8ba5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs @@ -87,5 +87,7 @@ namespace Avalonia.Markup.Xaml.Data /// Gets or sets a value that describes the type of relative source lookup. /// public RelativeSourceMode Mode { get; set; } + + public TreeType Tree { get; set; } = TreeType.Visual; } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 8984498393..c6705cbb4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -29,20 +29,167 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public override object ProvideValue(IServiceProvider serviceProvider) { + var descriptorContext = (ITypeDescriptorContext)serviceProvider; + + var pathInfo = ParsePath(Path, descriptorContext); + ValidateState(pathInfo); + return new Binding { Converter = Converter, ConverterParameter = ConverterParameter, - ElementName = ElementName, + ElementName = pathInfo.ElementName ?? ElementName, FallbackValue = FallbackValue, Mode = Mode, - Path = Path, + Path = pathInfo.Path, Priority = Priority, - RelativeSource = RelativeSource, + RelativeSource = pathInfo.RelativeSource ?? RelativeSource, DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) }; } + private class PathInfo + { + public string Path { get; set; } + public string ElementName { get; set; } + public RelativeSource RelativeSource { get; set; } + } + + private void ValidateState(PathInfo pathInfo) + { + if (pathInfo.ElementName != null && ElementName != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when an #elementName path is provided."); + } + + if (pathInfo.RelativeSource != null && RelativeSource != null) + { + throw new InvalidOperationException( + "ElementName property cannot be set when a $self or $parent path is provided."); + } + + if ((pathInfo.ElementName != null || ElementName != null) && + (pathInfo.RelativeSource != null || RelativeSource != null)) + { + throw new InvalidOperationException( + "ElementName property cannot be set with a RelativeSource."); + } + } + + private static PathInfo ParsePath(string path, ITypeDescriptorContext context) + { + var result = new PathInfo(); + + if (string.IsNullOrWhiteSpace(path) || path == ".") + { + result.Path = string.Empty; + } + else if (path.StartsWith("#")) + { + var dot = path.IndexOf('.'); + + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + result.ElementName = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + result.ElementName = path.Substring(1); + } + } + else if (path.StartsWith("$")) + { + var relativeSource = new RelativeSource + { + Tree = TreeType.Logical + }; + result.RelativeSource = relativeSource; + var dot = path.IndexOf('.'); + string relativeSourceMode; + if (dot != -1) + { + result.Path = path.Substring(dot + 1); + relativeSourceMode = path.Substring(1, dot - 1); + } + else + { + result.Path = string.Empty; + relativeSourceMode = path.Substring(1); + } + + if (relativeSourceMode == "self") + { + relativeSource.Mode = RelativeSourceMode.Self; + } + else if (relativeSourceMode == "parent") + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + relativeSource.AncestorLevel = 1; + } + else if (relativeSourceMode.StartsWith("parent[")) + { + relativeSource.Mode = RelativeSourceMode.FindAncestor; + var parentConfigStart = relativeSourceMode.IndexOf('['); + if (!relativeSourceMode.EndsWith("]")) + { + throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['."); + } + var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';'); + if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0) + { + throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax"); + } + else if (parentConfigParams.Length == 1) + { + if (int.TryParse(parentConfigParams[0], out int level)) + { + relativeSource.AncestorType = null; + relativeSource.AncestorLevel = level + 1; + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context); + } + } + else + { + relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context); + relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1; + } + } + else + { + throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}"); + } + } + else + { + result.Path = path; + } + + return result; + } + + private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context) + { + var parts = ancestorTypeName.Split(':'); + if (parts.Length == 0 || parts.Length > 2) + { + throw new InvalidOperationException("Invalid type name"); + } + + if (parts.Length == 1) + { + return context.ResolveType(string.Empty, parts[0]); + } + else + { + return context.ResolveType(parts[0], parts[1]); + } + } private static object GetDefaultAnchor(ITypeDescriptorContext context) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index 4664947b8e..c5fe83977f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs @@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ElementName = ElementName, Mode = Mode, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), - Path = Path, + Path = Path ?? string.Empty, Priority = Priority, }; } diff --git a/src/Markup/Avalonia.Markup/ControlLocator.cs b/src/Markup/Avalonia.Markup/ControlLocator.cs index de8415d6db..1a82c0a4fd 100644 --- a/src/Markup/Avalonia.Markup/ControlLocator.cs +++ b/src/Markup/Avalonia.Markup/ControlLocator.cs @@ -11,6 +11,21 @@ using Avalonia.VisualTree; namespace Avalonia.Markup { + /// + /// The type of tree via which to track a control. + /// + public enum TreeType + { + /// + /// The visual tree. + /// + Visual, + /// + /// The logical tree. + /// + Logical, + } + /// /// Locates controls relative to other controls. /// @@ -27,13 +42,13 @@ namespace Avalonia.Markup { var attached = Observable.FromEventPattern( x => relativeTo.AttachedToLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree += x) + x => relativeTo.AttachedToLogicalTree -= x) .Select(x => ((IControl)x.Sender).FindNameScope()) .StartWith(relativeTo.FindNameScope()); var detached = Observable.FromEventPattern( x => relativeTo.DetachedFromLogicalTree += x, - x => relativeTo.DetachedFromLogicalTree += x) + x => relativeTo.DetachedFromLogicalTree -= x) .Select(x => (INameScope)null); return attached.Merge(detached).Select(nameScope => @@ -68,37 +83,75 @@ namespace Avalonia.Markup /// /// The control relative from which the other control should be found. /// - /// The type of the ancestor to find. + /// The tree via which to track the control. /// /// The level of ancestor control to look for. Use 0 for the first ancestor of the /// requested type. /// - public static IObservable Track(IControl relativeTo, Type ancestorType, int ancestorLevel) + /// The type of the ancestor to find. + public static IObservable Track(IControl relativeTo, TreeType tree, int ancestorLevel, Type ancestorType = null) + { + return TrackAttachmentToTree(relativeTo, tree).Select(isAttachedToTree => + { + if (isAttachedToTree) + { + if (tree == TreeType.Visual) + { + return relativeTo.GetVisualAncestors() + .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(ancestorLevel) as IControl; + } + else + { + return relativeTo.GetLogicalAncestors() + .Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(ancestorLevel) as IControl; + } + } + else + { + return null; + } + }); + } + + private static IObservable TrackAttachmentToTree(IControl relativeTo, TreeType tree) + { + return tree == TreeType.Visual ? TrackAttachmentToVisualTree(relativeTo) : TrackAttachmentToLogicalTree(relativeTo); + } + + private static IObservable TrackAttachmentToVisualTree(IControl relativeTo) { var attached = Observable.FromEventPattern( x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.DetachedFromVisualTree += x) + x => relativeTo.AttachedToVisualTree -= x) .Select(x => true) .StartWith(relativeTo.IsAttachedToVisualTree); var detached = Observable.FromEventPattern( x => relativeTo.DetachedFromVisualTree += x, - x => relativeTo.DetachedFromVisualTree += x) + x => relativeTo.DetachedFromVisualTree -= x) .Select(x => false); - return attached.Merge(detached).Select(isAttachedToVisualTree => - { - if (isAttachedToVisualTree) - { - return relativeTo.GetVisualAncestors() - .Where(x => ancestorType.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo())) - .ElementAtOrDefault(ancestorLevel) as IControl; - } - else - { - return null; - } - }); + var attachmentStatus = attached.Merge(detached); + return attachmentStatus; + } + + private static IObservable TrackAttachmentToLogicalTree(IControl relativeTo) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToLogicalTree += x, + x => relativeTo.AttachedToLogicalTree -= x) + .Select(x => true) + .StartWith(relativeTo.IsAttachedToLogicalTree); + + var detached = Observable.FromEventPattern( + x => relativeTo.DetachedFromLogicalTree += x, + x => relativeTo.DetachedFromLogicalTree -= x) + .Select(x => false); + + var attachmentStatus = attached.Merge(detached); + return attachmentStatus; } } } diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs index a824a38867..563b372c78 100644 --- a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs @@ -51,15 +51,7 @@ namespace Avalonia.Markup.Data.Parsers } } - if (!r.End) - { - r.Take(); - return result; - } - else - { - throw new ExpressionParseException(r.Position, "Expected ']'."); - } + throw new ExpressionParseException(r.Position, "Expected ']'."); } return null; diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 6a72923ce3..b1bfdcbfeb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -54,7 +54,6 @@ namespace Avalonia.Direct2D1.Media _finishedCallback = finishedCallback; _directWriteFactory = directWriteFactory; _imagingFactory = imagingFactory; - _swapChain = swapChain; _renderTarget.BeginDraw(); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 4d6559a078..cd77d9cc88 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -387,6 +387,46 @@ namespace Avalonia.Base.UnitTests } } + [Fact] + public void IsAnimating_On_Property_With_No_Value_Returns_False() + { + var target = new Class1(); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Value_Returns_False() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Non_Animation_Binding_Returns_False() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.LocalValue); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Binding_Returns_True() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + /// /// Returns an observable that returns a single value but does not complete. /// diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 587816b07b..fd731455d8 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -83,6 +83,28 @@ namespace Avalonia.Base.UnitTests.Collections Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target); } + [Fact] + public void MoveRange_Raises_Correct_CollectionChanged_Event() + { + var target = new AvaloniaList(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + var raised = false; + + target.CollectionChanged += (s, e) => + { + Assert.Equal(NotifyCollectionChangedAction.Move, e.Action); + Assert.Equal(0, e.OldStartingIndex); + Assert.Equal(10, e.NewStartingIndex); + Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.OldItems); + Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.NewItems); + raised = true; + }; + + target.MoveRange(0, 9, 10); + + Assert.True(raised); + Assert.Equal(new[] { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, target); + } + [Fact] public void Adding_Item_Should_Raise_CollectionChanged() { diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index 3a37585dc0..84ff492512 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.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.Subjects; using Avalonia.Data; using Xunit; @@ -70,6 +71,17 @@ namespace Avalonia.Base.UnitTests Assert.Same(p1.Initialized, p2.Initialized); } + [Fact] + public void IsAnimating_On_DirectProperty_With_Binding_Returns_False() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index fb1ae3ba1a..29acdaa8d4 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Collections; using Avalonia.LogicalTree; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests @@ -18,8 +19,9 @@ namespace Avalonia.Controls.UnitTests panel.Children.Add(child); - Assert.Equal(child.Parent, panel); - Assert.Equal(child.GetLogicalParent(), panel); + Assert.Same(child.Parent, panel); + Assert.Same(child.GetLogicalParent(), panel); + Assert.Same(child.GetVisualParent(), panel); } [Fact] @@ -45,6 +47,7 @@ namespace Avalonia.Controls.UnitTests Assert.Null(child.Parent); Assert.Null(child.GetLogicalParent()); + Assert.Null(child.GetVisualParent()); } [Fact] @@ -60,8 +63,10 @@ namespace Avalonia.Controls.UnitTests Assert.Null(child1.Parent); Assert.Null(child1.GetLogicalParent()); + Assert.Null(child1.GetVisualParent()); Assert.Null(child2.Parent); Assert.Null(child2.GetLogicalParent()); + Assert.Null(child2.GetVisualParent()); } [Fact] @@ -77,24 +82,32 @@ namespace Avalonia.Controls.UnitTests Assert.Null(child1.Parent); Assert.Null(child1.GetLogicalParent()); + Assert.Null(child1.GetVisualParent()); Assert.Null(child2.Parent); Assert.Null(child2.GetLogicalParent()); + Assert.Null(child2.GetVisualParent()); } [Fact] - public void Setting_Children_Should_Make_Controls_Appear_In_Panel_Children() + public void Replacing_Panel_Children_Should_Clear_And_Set_Control_Parent() { var panel = new Panel(); - var child = new Control(); + var child1 = new Control(); + var child2 = new Control(); - panel.Children = new Controls { child }; + panel.Children.Add(child1); + panel.Children[0] = child2; - Assert.Equal(new[] { child }, panel.Children); - Assert.Equal(new[] { child }, panel.GetLogicalChildren()); + Assert.Null(child1.Parent); + Assert.Null(child1.GetLogicalParent()); + Assert.Null(child1.GetVisualParent()); + Assert.Same(child2.Parent, panel); + Assert.Same(child2.GetLogicalParent(), panel); + Assert.Same(child2.GetVisualParent(), panel); } [Fact] - public void Child_Control_Should_Appear_In_Panel_Children() + public void Child_Control_Should_Appear_In_Panel_Logical_And_Visual_Children() { var panel = new Panel(); var child = new Control(); @@ -103,10 +116,11 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child }, panel.Children); Assert.Equal(new[] { child }, panel.GetLogicalChildren()); + Assert.Equal(new[] { child }, panel.GetVisualChildren()); } [Fact] - public void Removing_Child_Control_Should_Remove_From_Panel_Children() + public void Removing_Child_Control_Should_Remove_From_Panel_Logical_And_Visual_Children() { var panel = new Panel(); var child = new Control(); @@ -115,7 +129,36 @@ namespace Avalonia.Controls.UnitTests panel.Children.Remove(child); Assert.Equal(new Control[0], panel.Children); - Assert.Equal(new ILogical[0], panel.GetLogicalChildren()); + Assert.Empty(panel.GetLogicalChildren()); + Assert.Empty(panel.GetVisualChildren()); + } + + [Fact] + public void Moving_Panel_Children_Should_Reoder_Logical_And_Visual_Children() + { + var panel = new Panel(); + var child1 = new Control(); + var child2 = new Control(); + + panel.Children.Add(child1); + panel.Children.Add(child2); + panel.Children.Move(1, 0); + + Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren()); + Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren()); + } + + [Fact] + public void Setting_Children_Should_Make_Controls_Appear_In_Logical_And_Visual_Children() + { + var panel = new Panel(); + var child = new Control(); + + panel.Children = new Controls { child }; + + Assert.Equal(new[] { child }, panel.Children); + Assert.Equal(new[] { child }, panel.GetLogicalChildren()); + Assert.Equal(new[] { child }, panel.GetVisualChildren()); } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 230e61f300..9a08073920 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -13,6 +13,7 @@ using Moq; using Xunit; using System.ComponentModel; using System.Runtime.CompilerServices; +using Avalonia.UnitTests; namespace Avalonia.Markup.Xaml.UnitTests.Data { @@ -337,6 +338,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("foo", target.Content); } + [Fact] + public void Binding_With_Null_Path_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = "foo"; + window.ApplyTemplate(); + + Assert.Equal("foo", textBlock.Text); + } + } + private class TwoWayBindingTest : Control { public static readonly StyledProperty TwoWayProperty = diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs index 197afe46ee..ccb13039f1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs @@ -11,6 +11,9 @@ using Avalonia.Markup.Xaml.Data; using Avalonia.Styling; using Xunit; using System.Reactive.Disposables; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using System.Linq; namespace Avalonia.Markup.Xaml.UnitTests.Data { @@ -56,6 +59,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data BindingPriority.TemplatedParent)); } + [Fact] + public void TemplateBinding_With_Null_Path_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var button = window.FindControl