A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

619 lines
22 KiB

// 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 System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Rendering;
using Avalonia.VisualTree;
namespace Avalonia
{
/// <summary>
/// Base class for controls that provides rendering and related visual properties.
/// </summary>
/// <remarks>
/// The <see cref="Visual"/> class represents elements that have a visual on-screen
/// representation and stores all the information needed for an
/// <see cref="IRenderer"/> to render the control. To traverse the visual tree, use the
/// extension methods defined in <see cref="VisualExtensions"/>.
/// </remarks>
[UsableDuringInitialization]
public class Visual : StyledElement, IVisual
{
/// <summary>
/// Defines the <see cref="Bounds"/> property.
/// </summary>
public static readonly DirectProperty<Visual, Rect> BoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, Rect>(nameof(Bounds), o => o.Bounds);
public static readonly DirectProperty<Visual, TransformedBounds?> TransformedBoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, TransformedBounds?>(
nameof(TransformedBounds),
o => o.TransformedBounds);
/// <summary>
/// Defines the <see cref="ClipToBounds"/> property.
/// </summary>
public static readonly StyledProperty<bool> ClipToBoundsProperty =
AvaloniaProperty.Register<Visual, bool>(nameof(ClipToBounds));
/// <summary>
/// Defines the <see cref="Clip"/> property.
/// </summary>
public static readonly StyledProperty<Geometry> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisibleProperty"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsVisibleProperty =
AvaloniaProperty.Register<Visual, bool>(nameof(IsVisible), true);
/// <summary>
/// Defines the <see cref="Opacity"/> property.
/// </summary>
public static readonly StyledProperty<double> OpacityProperty =
AvaloniaProperty.Register<Visual, double>(nameof(Opacity), 1);
/// <summary>
/// Defines the <see cref="OpacityMask"/> property.
/// </summary>
public static readonly StyledProperty<IBrush> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="RenderTransform"/> property.
/// </summary>
public static readonly StyledProperty<Transform> RenderTransformProperty =
AvaloniaProperty.Register<Visual, Transform>(nameof(RenderTransform));
/// <summary>
/// Defines the <see cref="RenderTransformOrigin"/> property.
/// </summary>
public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="IVisual.VisualParent"/> property.
/// </summary>
public static readonly DirectProperty<Visual, IVisual> VisualParentProperty =
AvaloniaProperty.RegisterDirect<Visual, IVisual>("VisualParent", o => o._visualParent);
/// <summary>
/// Defines the <see cref="ZIndex"/> property.
/// </summary>
public static readonly StyledProperty<int> ZIndexProperty =
AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
private Rect _bounds;
private TransformedBounds? _transformedBounds;
private IRenderRoot _visualRoot;
private IVisual _visualParent;
/// <summary>
/// Initializes static members of the <see cref="Visual"/> class.
/// </summary>
static Visual()
{
AffectsRender<Visual>(
BoundsProperty,
ClipProperty,
ClipToBoundsProperty,
IsVisibleProperty,
OpacityProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
}
/// <summary>
/// Initializes a new instance of the <see cref="Visual"/> class.
/// </summary>
public Visual()
{
var visualChildren = new AvaloniaList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = ValidateVisualChild;
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
/// <summary>
/// Raised when the control is attached to a rooted visual tree.
/// </summary>
public event EventHandler<VisualTreeAttachmentEventArgs> AttachedToVisualTree;
/// <summary>
/// Raised when the control is detached from a rooted visual tree.
/// </summary>
public event EventHandler<VisualTreeAttachmentEventArgs> DetachedFromVisualTree;
/// <summary>
/// Gets the bounds of the control relative to its parent.
/// </summary>
public Rect Bounds
{
get { return _bounds; }
protected set { SetAndRaise(BoundsProperty, ref _bounds, value); }
}
/// <summary>
/// Gets the bounds of the control relative to the window, accounting for rendering transforms.
/// </summary>
public TransformedBounds? TransformedBounds => _transformedBounds;
/// <summary>
/// Gets a value indicating whether the control should be clipped to its bounds.
/// </summary>
public bool ClipToBounds
{
get { return GetValue(ClipToBoundsProperty); }
set { SetValue(ClipToBoundsProperty, value); }
}
/// <summary>
/// Gets or sets the geometry clip for this visual.
/// </summary>
public Geometry Clip
{
get { return GetValue(ClipProperty); }
set { SetValue(ClipProperty, value); }
}
/// <summary>
/// Gets a value indicating whether this control and all its parents are visible.
/// </summary>
public bool IsEffectivelyVisible
{
get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); }
}
/// <summary>
/// Gets a value indicating whether this control is visible.
/// </summary>
public bool IsVisible
{
get { return GetValue(IsVisibleProperty); }
set { SetValue(IsVisibleProperty, value); }
}
/// <summary>
/// Gets the opacity of the control.
/// </summary>
public double Opacity
{
get { return GetValue(OpacityProperty); }
set { SetValue(OpacityProperty, value); }
}
/// <summary>
/// Gets the opacity mask of the control.
/// </summary>
public IBrush OpacityMask
{
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets the render transform of the control.
/// </summary>
public Transform RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
}
/// <summary>
/// Gets the transform origin of the control.
/// </summary>
public RelativePoint RenderTransformOrigin
{
get { return GetValue(RenderTransformOriginProperty); }
set { SetValue(RenderTransformOriginProperty, value); }
}
/// <summary>
/// Gets the Z index of the control.
/// </summary>
/// <remarks>
/// Controls with a higher <see cref="ZIndex"/> 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.
/// </remarks>
public int ZIndex
{
get { return GetValue(ZIndexProperty); }
set { SetValue(ZIndexProperty, value); }
}
/// <summary>
/// Gets the control's child visuals.
/// </summary>
protected IAvaloniaList<IVisual> VisualChildren
{
get;
private set;
}
/// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
protected IRenderRoot VisualRoot => _visualRoot ?? (this as IRenderRoot);
/// <summary>
/// Gets a value indicating whether this control is attached to a visual root.
/// </summary>
bool IVisual.IsAttachedToVisualTree => VisualRoot != null;
/// <summary>
/// Gets the control's child controls.
/// </summary>
IAvaloniaReadOnlyList<IVisual> IVisual.VisualChildren => VisualChildren;
/// <summary>
/// Gets the control's parent visual.
/// </summary>
IVisual IVisual.VisualParent => _visualParent;
/// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
IRenderRoot IVisual.VisualRoot => VisualRoot;
TransformedBounds? IVisual.TransformedBounds
{
get { return _transformedBounds; }
set { SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value); }
}
/// <summary>
/// Invalidates the visual and queues a repaint.
/// </summary>
public void InvalidateVisual()
{
VisualRoot?.Renderer?.AddDirty(this);
}
/// <summary>
/// Renders the visual to a <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
public virtual void Render(DrawingContext context)
{
Contract.Requires<ArgumentNullException>(context != null);
}
/// <summary>
/// Returns a transform that transforms the visual's coordinates into the coordinates
/// of the specified <paramref name="visual"/>.
/// </summary>
/// <param name="visual">The visual to translate the coordinates to.</param>
/// <returns>
/// A <see cref="Matrix"/> containing the transform or null if the visuals don't share a
/// common ancestor.
/// </returns>
public Matrix? TransformToVisual(IVisual visual)
{
var common = this.FindCommonVisualAncestor(visual);
if (common != null)
{
var thisOffset = GetOffsetFrom(common, this);
var thatOffset = GetOffsetFrom(common, visual);
return -thatOffset * thisOffset;
}
return null;
}
/// <summary>
/// Indicates that a property change should cause <see cref="InvalidateVisual"/> to be
/// called.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// 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.
/// </remarks>
[Obsolete("Use AffectsRender<T> and specify the control type.")]
protected static void AffectsRender(params AvaloniaProperty[] properties)
{
AffectsRender<Visual>(properties);
}
/// <summary>
/// Indicates that a property change should cause <see cref="InvalidateVisual"/> to be
/// called.
/// </summary>
/// <typeparam name="T">The control which the property affects.</typeparam>
/// <param name="properties">The properties.</param>
/// <remarks>
/// 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.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Visual
{
void Invalidate(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is T sender)
{
if (e.OldValue is IAffectsRender oldValue)
{
oldValue.Invalidated -= sender.AffectsRenderInvalidated;
}
if (e.NewValue is IAffectsRender newValue)
{
newValue.Invalidated += sender.AffectsRenderInvalidated;
}
sender.InvalidateVisual();
}
}
foreach (var property in properties)
{
property.Changed.Subscribe(Invalidate);
}
}
/// <summary>
/// Calls the <see cref="OnAttachedToVisualTree(VisualTreeAttachmentEventArgs)"/> method
/// for this control and all of its visual descendants.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
Logger.Verbose(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<Visual>())
{
child.OnAttachedToVisualTreeCore(e);
}
}
}
/// <summary>
/// Calls the <see cref="OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs)"/> method
/// for this control and all of its visual descendants.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
Logger.Verbose(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<Visual>())
{
child.OnDetachedFromVisualTreeCore(e);
}
}
}
/// <summary>
/// Called when the control is added to a visual tree.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
}
/// <summary>
/// Called when the control is removed from a visual tree.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
}
/// <summary>
/// Called when the control's visual parent changes.
/// </summary>
/// <param name="oldParent">The old visual parent.</param>
/// <param name="newParent">The new visual parent.</param>
protected virtual void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
}
/// <summary>
/// Gets the visual offset from the specified ancestor.
/// </summary>
/// <param name="ancestor">The ancestor visual.</param>
/// <param name="visual">The visual.</param>
/// <returns>The visual offset.</returns>
private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual)
{
var result = Matrix.Identity;
while (visual != ancestor)
{
if (visual.RenderTransform?.Value != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size);
var offset = Matrix.CreateTranslation(origin);
var renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
result *= renderTransform;
}
var topLeft = visual.Bounds.TopLeft;
if (topLeft != default)
{
result *= Matrix.CreateTranslation(topLeft);
}
visual = visual.VisualParent;
if (visual == null)
{
throw new ArgumentException("'visual' is not a descendant of 'ancestor'.");
}
}
return result;
}
/// <summary>
/// Called when a visual's <see cref="RenderTransform"/> changes.
/// </summary>
/// <param name="e">The event args.</param>
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();
}
}
/// <summary>
/// Ensures a visual child is not null and not already parented.
/// </summary>
/// <param name="c">The visual child.</param>
private static void ValidateVisualChild(IVisual c)
{
if (c == null)
{
throw new ArgumentNullException("Cannot add null to VisualChildren.");
}
if (c.VisualParent != null)
{
throw new InvalidOperationException("The control already has a visual parent.");
}
}
/// <summary>
/// Called when the <see cref="RenderTransform"/>'s <see cref="Transform.Changed"/> event
/// is fired.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void RenderTransformChanged(object sender, EventArgs e)
{
InvalidateVisual();
}
/// <summary>
/// Sets the visual parent of the Visual.
/// </summary>
/// <param name="value">The visual parent.</param>
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.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
OnAttachedToVisualTreeCore(e);
}
OnVisualParentChanged(old, value);
}
private void AffectsRenderInvalidated(object sender, EventArgs e) => InvalidateVisual();
/// <summary>
/// Called when the <see cref="VisualChildren"/> collection changes.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
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;
}
}
}
}