File: System\Windows\Media\GeometryHitTestParameters.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//
 
using MS.Internal;
 
namespace System.Windows.Media
{
    /// <summary>
    /// This is the class for specifying parameters hit testing with a geometry.
    /// </summary>
    public class GeometryHitTestParameters : HitTestParameters
    {
        private PathGeometry _hitGeometryInternal;  // The internal geometry we use for hit testing
        private Geometry _hitGeometryCache = null;  // Cached frozen copy we hand out from HitGeometry property.
        private Rect _origBounds;
        private Rect _bounds;
        private MatrixStack _matrixStack;
 
        /// <summary>
        /// The constructor takes the geometry to hit test with.
        /// </summary>
        public GeometryHitTestParameters(Geometry geometry) : base()
        {
            // This ctor guarantees that we are initialized in the following way:
            //
            //  1.  The geometry provided by the user is unmodified
            //  2.  _hitGeometryInternal is a PathGeometry equivilent to the supplied geometry
            //  3.  _hitGeometryInternal.Tranform is a MatrixTransform equivilent to the supplied
            //      geometry.Transform
            //  4.  _origBounds is the untransformed bounds of the _hitGeometryInternal (inner space).
            //  5.  _bounds is the transformed bounds of the _hitGeometryInternal (outer space).
            //  6.  _matrixStack is an empty stack.            
 
            ArgumentNullException.ThrowIfNull(geometry);
 
            // Convert the Geometry to an equivilent PathGeometry up front to prevent
            // conversion on every call to DoesContainWithDetail.  If the geometry is
            // animate this also has the side effect of eliminating animation interplay.
            _hitGeometryInternal = geometry.GetAsPathGeometry();
 
            // If GetAsPathGeometry was a no-op, force the copy because we do not
            // went to modify the user's Geometry.
            if (object.ReferenceEquals(_hitGeometryInternal, geometry))
            {
                _hitGeometryInternal = _hitGeometryInternal.Clone();
            }
            
            // Convert _hitGeometryInternal.Transform to an equivilent MatrixTransform
            // so that we can aggregate in PushMatrix/PopMatrix without building a
            // TransformCollection.
            Transform origTransform = _hitGeometryInternal.Transform;            
            MatrixTransform newTransform = new MatrixTransform();
 
            _hitGeometryInternal.Transform = newTransform;
 
            // Before we initialize MatrixTransform.Matrix, cache the bounds of this
            // geometry without any transforms.
            _origBounds = _hitGeometryInternal.Bounds;
 
            // If we had a non-Identity transform, update our MatrixTransform.Matrix
            // with its Value.  (Note that when GetAsPathGeometry *isn't* a no-op
            // it applies the transform to the figures and returns with a Geometry
            // with an identity Transform.)
            if (origTransform != null && !origTransform.IsIdentity)
            {
                newTransform.Matrix = origTransform.Value;
            }
            
            // Initialize the current transformed bounds of this Geometry
            _bounds = _hitGeometryInternal.Bounds;
            _matrixStack = new MatrixStack();
        }
    
        /// <summary>
        /// The geometry to hit test against.  This will be a frozen copy of the
        /// hit geometry transformed into the local space of the Visual being hit
        /// tested.
        /// </summary>
        public Geometry HitGeometry
        {
            get
            {
                if (_hitGeometryCache == null)
                {
                    _hitGeometryCache = (Geometry) _hitGeometryInternal.GetAsFrozen();
                }
 
                Debug.Assert(_hitGeometryInternal.Transform.Value == _hitGeometryCache.Transform.Value,
                    "HitGeometry has a different transform than HitGeometryInternal. Did we forget to invalidate the cache?");
                
                return _hitGeometryCache;
            }
        }
 
        // Use this property when retrieving the hit geometry internally.  Right now it avoids a
        // a cast.  In the future it will avoid a copy as well. 
        // There is a bug that allows the HitGeometry returned from GeometryHitTestParamaters
        // to be unintentially mutable, allowing users to modify the hit geometry or attach
        // changed handlers after the GeometryHitTestParamaters is constructed.
        // The value from this property should never be exposed to the user (e.g., do not use it to return
        // hit test results.)
 
        internal PathGeometry InternalHitGeometry
        {
            get
            {
                return _hitGeometryInternal;
            }
        }
 
        internal Rect Bounds
        {
            get 
            {
                return _bounds;
            }
        }
 
        internal void PushMatrix(ref Matrix newMatrix)
        {
            MatrixTransform matrixTransform = (MatrixTransform) _hitGeometryInternal.Transform;
 
            // Grab the old composedMatrix from our MatrixTransform and save
            // it on our MatrixStack.
            Matrix composedMatrix = matrixTransform.Value;
            _matrixStack.Push(ref composedMatrix, false);
            
            // Compose the new matrix into our MatrixTransform
            MatrixUtil.MultiplyMatrix(ref composedMatrix, ref newMatrix);
            
            matrixTransform.Matrix = composedMatrix;
            
            // Update active bounds based on new tranform.
            _bounds = Rect.Transform(_origBounds, composedMatrix);
 
            ClearHitGeometryCache();
        }
 
        internal void PopMatrix()
        {
            Matrix matrix = _matrixStack.Peek();
 
            // Restore saved transform.
            ((MatrixTransform) (_hitGeometryInternal.Transform)).Matrix = matrix;
            
            // Update active bounds based on new X-form.
            _bounds = Rect.Transform(_origBounds, matrix);
            
            _matrixStack.Pop();
 
            ClearHitGeometryCache();
        }
 
        // This method is called to restore the _hitGeometryInternal's transform to its original state
        // in the event that a user's delegate threw an exception during the hit test walk.
        internal void EmergencyRestoreOriginalTransform()
        {
            // Replace the current transform with the first matrix pushed onto the stack.
            Matrix matrix = ((MatrixTransform) (_hitGeometryInternal.Transform)).Matrix;
            
            while (!_matrixStack.IsEmpty)
            {
                matrix = _matrixStack.Peek();
                _matrixStack.Pop();
            }
 
            ((MatrixTransform) (_hitGeometryInternal.Transform)).Matrix = matrix;
 
            ClearHitGeometryCache();
        }
 
        // This clears the 
        private void ClearHitGeometryCache()
        {
            _hitGeometryCache = null;
        }
    }
}