// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Rendering;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia
{
///
/// Base class for controls that provides rendering and related visual properties.
///
///
/// The class represents elements that have a visual on-screen
/// representation and stores all the information needed for an
/// to render the control. To traverse the visual tree, use the
/// extension methods defined in .
///
[UsableDuringInitialization]
public class Visual : StyledElement, IVisual
{
///
/// Defines the property.
///
public static readonly DirectProperty BoundsProperty =
AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds);
public static readonly DirectProperty TransformedBoundsProperty =
AvaloniaProperty.RegisterDirect(
nameof(TransformedBounds),
o => o.TransformedBounds);
///
/// Defines the property.
///
public static readonly StyledProperty ClipToBoundsProperty =
AvaloniaProperty.Register(nameof(ClipToBounds));
///
/// Defines the property.
///
public static readonly StyledProperty ClipProperty =
AvaloniaProperty.Register(nameof(Clip));
///
/// Defines the property.
///
public static readonly StyledProperty IsVisibleProperty =
AvaloniaProperty.Register(nameof(IsVisible), true);
///
/// Defines the property.
///
public static readonly StyledProperty OpacityProperty =
AvaloniaProperty.Register(nameof(Opacity), 1);
///
/// Defines the property.
///
public static readonly StyledProperty OpacityMaskProperty =
AvaloniaProperty.Register(nameof(OpacityMask));
///
/// Defines the property.
///
public static readonly StyledProperty RenderTransformProperty =
AvaloniaProperty.Register(nameof(RenderTransform));
///
/// Defines the property.
///
public static readonly StyledProperty RenderTransformOriginProperty =
AvaloniaProperty.Register(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
///
/// Defines the property.
///
public static readonly DirectProperty VisualParentProperty =
AvaloniaProperty.RegisterDirect("VisualParent", o => o._visualParent);
///
/// Defines the property.
///
public static readonly StyledProperty ZIndexProperty =
AvaloniaProperty.Register(nameof(ZIndex));
private Rect _bounds;
private TransformedBounds? _transformedBounds;
private IRenderRoot _visualRoot;
private IVisual _visualParent;
///
/// Initializes static members of the class.
///
static Visual()
{
AffectsRender(
BoundsProperty,
ClipProperty,
ClipToBoundsProperty,
IsVisibleProperty,
OpacityProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
ZIndexProperty.Changed.Subscribe(ZIndexChanged);
}
///
/// Initializes a new instance of the class.
///
public Visual()
{
var visualChildren = new AvaloniaList();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = visual => ValidateVisualChild(visual);
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
///
/// Raised when the control is attached to a rooted visual tree.
///
public event EventHandler AttachedToVisualTree;
///
/// Raised when the control is detached from a rooted visual tree.
///
public event EventHandler DetachedFromVisualTree;
///
/// Gets the bounds of the control relative to its parent.
///
public Rect Bounds
{
get { return _bounds; }
protected set { SetAndRaise(BoundsProperty, ref _bounds, value); }
}
///
/// Gets the bounds of the control relative to the window, accounting for rendering transforms.
///
public TransformedBounds? TransformedBounds => _transformedBounds;
///
/// Gets a value indicating whether the control should be clipped to its bounds.
///
public bool ClipToBounds
{
get { return GetValue(ClipToBoundsProperty); }
set { SetValue(ClipToBoundsProperty, value); }
}
///
/// Gets or sets the geometry clip for this visual.
///
public Geometry Clip
{
get { return GetValue(ClipProperty); }
set { SetValue(ClipProperty, value); }
}
///
/// Gets a value indicating whether this control and all its parents are visible.
///
public bool IsEffectivelyVisible
{
get
{
IVisual node = this;
while (node != null)
{
if (!node.IsVisible)
{
return false;
}
node = node.VisualParent;
}
return true;
}
}
///
/// Gets a value indicating whether this control is visible.
///
public bool IsVisible
{
get { return GetValue(IsVisibleProperty); }
set { SetValue(IsVisibleProperty, value); }
}
///
/// Gets the opacity of the control.
///
public double Opacity
{
get { return GetValue(OpacityProperty); }
set { SetValue(OpacityProperty, value); }
}
///
/// Gets the opacity mask of the control.
///
public IBrush OpacityMask
{
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
///
/// Gets the render transform of the control.
///
public Transform RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
}
///
/// Gets the transform origin of the control.
///
public RelativePoint RenderTransformOrigin
{
get { return GetValue(RenderTransformOriginProperty); }
set { SetValue(RenderTransformOriginProperty, value); }
}
///
/// Gets the Z index of the control.
///
///
/// Controls with a higher will appear in front of controls with
/// a lower ZIndex. If two controls have the same ZIndex then the control that appears
/// later in the containing element's children collection will appear on top.
///
public int ZIndex
{
get { return GetValue(ZIndexProperty); }
set { SetValue(ZIndexProperty, value); }
}
///
/// Gets the control's child visuals.
///
protected IAvaloniaList VisualChildren
{
get;
private set;
}
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot);
///
/// Gets a value indicating whether this control is attached to a visual root.
///
bool IVisual.IsAttachedToVisualTree => VisualRoot != null;
///
/// Gets the control's child controls.
///
IAvaloniaReadOnlyList IVisual.VisualChildren => VisualChildren;
///
/// Gets the control's parent visual.
///
IVisual IVisual.VisualParent => _visualParent;
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
IRenderRoot IVisual.VisualRoot => VisualRoot;
TransformedBounds? IVisual.TransformedBounds
{
get { return _transformedBounds; }
set { SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); }
}
///
/// Invalidates the visual and queues a repaint.
///
public void InvalidateVisual()
{
VisualRoot?.Renderer?.AddDirty(this);
}
///
/// Renders the visual to a .
///
/// The drawing context.
public virtual void Render(DrawingContext context)
{
Contract.Requires(context != null);
}
///
/// Indicates that a property change should cause to be
/// called.
///
/// The properties.
///
/// This method should be called in a control's static constructor with each property
/// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag.
///
[Obsolete("Use AffectsRender and specify the control type.")]
protected static void AffectsRender(params AvaloniaProperty[] properties)
{
AffectsRender(properties);
}
///
/// Indicates that a property change should cause to be
/// called.
///
/// The control which the property affects.
/// The properties.
///
/// This method should be called in a control's static constructor with each property
/// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag.
///
protected static void AffectsRender(params AvaloniaProperty[] properties)
where T : Visual
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is T sender)
{
if (e.OldValue is IAffectsRender oldValue)
{
WeakEventHandlerManager.Unsubscribe(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated);
}
if (e.NewValue is IAffectsRender newValue)
{
WeakEventHandlerManager.Subscribe(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated);
}
sender.InvalidateVisual();
}
}
foreach (var property in properties)
{
property.Changed.Subscribe(Invalidate);
}
}
protected override void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
VisualRoot?.Renderer?.RecalculateChildren(this);
}
///
/// Calls the method
/// for this control and all of its visual descendants.
///
/// The event args.
protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Attached to visual tree");
_visualRoot = e.Root;
if (RenderTransform != null)
{
RenderTransform.Changed += RenderTransformChanged;
}
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
if (VisualChildren != null)
{
foreach (Visual child in VisualChildren.OfType())
{
child.OnAttachedToVisualTreeCore(e);
}
}
}
///
/// Calls the method
/// for this control and all of its visual descendants.
///
/// The event args.
protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
Logger.TryGet(LogEventLevel.Verbose)?.Log(LogArea.Visual, this, "Detached from visual tree");
_visualRoot = null;
if (RenderTransform != null)
{
RenderTransform.Changed -= RenderTransformChanged;
}
OnDetachedFromVisualTree(e);
DetachedFromVisualTree?.Invoke(this, e);
e.Root?.Renderer?.AddDirty(this);
if (VisualChildren != null)
{
foreach (Visual child in VisualChildren.OfType())
{
child.OnDetachedFromVisualTreeCore(e);
}
}
}
///
/// Called when the control is added to a visual tree.
///
/// The event args.
protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
}
///
/// Called when the control is removed from a visual tree.
///
/// The event args.
protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
}
///
/// Called when the control's visual parent changes.
///
/// The old visual parent.
/// The new visual parent.
protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
}
protected override sealed void LogBindingError(AvaloniaProperty property, Exception e)
{
// Don't log a binding error unless the control is attached to a logical or visual tree.
// In theory this should only need to check for logical tree attachment, but in practise
// due to ContentControlMixin only taking effect when the template has finished being
// applied, some controls are attached to the visual tree before the logical tree.
if (((ILogical)this).IsAttachedToLogicalTree || ((IVisual)this).IsAttachedToVisualTree)
{
if (e is BindingChainException b &&
string.IsNullOrEmpty(b.ExpressionErrorPoint) &&
DataContext == null)
{
// The error occurred at the root of the binding chain and DataContext is null;
// don't log this - the DataContext probably hasn't been set up yet.
return;
}
Logger.TryGet(LogEventLevel.Warning)?.Log(
LogArea.Binding,
this,
"Error in binding to {Target}.{Property}: {Message}",
this,
property,
e.Message);
}
}
///
/// Called when a visual's changes.
///
/// The event args.
private static void RenderTransformChanged(AvaloniaPropertyChangedEventArgs e)
{
var sender = e.Sender as Visual;
if (sender?.VisualRoot != null)
{
var oldValue = e.OldValue as Transform;
var newValue = e.NewValue as Transform;
if (oldValue != null)
{
oldValue.Changed -= sender.RenderTransformChanged;
}
if (newValue != null)
{
newValue.Changed += sender.RenderTransformChanged;
}
sender.InvalidateVisual();
}
}
///
/// Ensures a visual child is not null and not already parented.
///
/// The visual child.
private static void ValidateVisualChild(IVisual c)
{
if (c == null)
{
throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren.");
}
if (c.VisualParent != null)
{
throw new InvalidOperationException("The control already has a visual parent.");
}
}
///
/// Called when the property changes on any control.
///
/// The event args.
private static void ZIndexChanged(AvaloniaPropertyChangedEventArgs e)
{
var sender = e.Sender as IVisual;
var parent = sender?.VisualParent;
sender?.InvalidateVisual();
parent?.VisualRoot?.Renderer?.RecalculateChildren(parent);
}
///
/// Called when the 's event
/// is fired.
///
/// The sender.
/// The event args.
private void RenderTransformChanged(object sender, EventArgs e)
{
InvalidateVisual();
}
///
/// Sets the visual parent of the Visual.
///
/// The visual parent.
private void SetVisualParent(Visual value)
{
if (_visualParent == value)
{
return;
}
var old = _visualParent;
_visualParent = value;
if (_visualRoot != null)
{
var e = new VisualTreeAttachmentEventArgs(old, VisualRoot);
OnDetachedFromVisualTreeCore(e);
}
if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
{
var root = this.FindAncestorOfType();
var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
OnAttachedToVisualTreeCore(e);
}
OnVisualParentChanged(old, value);
}
private void AffectsRenderInvalidated(object sender, EventArgs e) => InvalidateVisual();
///
/// Called when the collection changes.
///
/// The sender.
/// The event args.
private void VisualChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (Visual v in e.NewItems)
{
v.SetVisualParent(this);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Visual v in e.OldItems)
{
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;
}
}
}
}