// 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; } } } }