File: MS\Internal\Ink\Renderer.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 System.Windows.Media;
using MS.Internal.Ink;
using MS.Internal.PresentationCore;
 
namespace System.Windows.Ink
{
    /// <summary>
    /// The Renderer class is used to render a stroke collection.
    /// This class listens to stroke added and removed events on the
    /// stroke collection and updates the internal visual tree.
    /// It also listens to the Invalidated event on Stroke (fired when
    /// DrawingAttributes changes, packet data changes, DrawingAttributes
    /// replaced, as well as Stroke developer calls Stroke.OnInvalidated)
    /// and updates the visual state as necessary.
    /// </summary>
    ///
    internal class Renderer
    {
        #region StrokeVisual
 
        /// <summary>
        /// A retained visual for rendering a single stroke in a stroke collection view
        /// </summary>
        private class StrokeVisual : System.Windows.Media.DrawingVisual
        {
            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="stroke">a stroke to render into this visual</param>
            /// <param name="renderer">a renderer associated to this visual</param>
            internal StrokeVisual(Stroke stroke, Renderer renderer) : base()
            {
                Debug.Assert(renderer != null);
 
                ArgumentNullException.ThrowIfNull(stroke);
 
                _stroke = stroke;
                _renderer = renderer;
 
                // The original value of the color and IsHighlighter are cached so
                // when Stroke.Invalidated is fired, Renderer knows whether re-arranging
                // the visual tree is needed.
                _cachedColor = stroke.DrawingAttributes.Color;
                _cachedIsHighlighter = stroke.DrawingAttributes.IsHighlighter;
 
                // Update the visual contents
                Update();
            }
 
            /// <summary>
            /// The Stroke rendedered into this visual.
            /// </summary>
            internal Stroke Stroke
            {
                get { return _stroke; }
            }
            
            /// <summary>
            /// Updates the contents of the visual.
            /// </summary>
            internal void Update()
            {
                using (DrawingContext drawingContext = RenderOpen())
                {
                    bool highContrast = _renderer.IsHighContrast();
 
                    if (highContrast == true && _stroke.DrawingAttributes.IsHighlighter)
                    {
                        // we don't render highlighters in high contrast
                        return;
                    }
 
                    DrawingAttributes da;
                    if (highContrast)
                    {
                        da = _stroke.DrawingAttributes.Clone();
                        da.Color = _renderer.GetHighContrastColor();
                    }
                    else if (_stroke.DrawingAttributes.IsHighlighter == true)
                    {
                        // Get the drawing attributes to use for a highlighter stroke. This can be a copied DA with color.A
                        // overridden if color.A != 255.
                        da = StrokeRenderer.GetHighlighterAttributes(_stroke, _stroke.DrawingAttributes);
                    }
                    else
                    {
                        // Otherwise, usethe DA on this stroke
                        da = _stroke.DrawingAttributes;
                    }
 
                    // Draw selected stroke as hollow
                    _stroke.DrawInternal (drawingContext, da, _stroke.IsSelected );
                }
            }
 
            /// <summary>
            /// StrokeVisual should not be hittestable as it interferes with event routing
            /// </summary>
            protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParams)
            {
                return null;
            }
 
            /// <summary>
            /// The previous value of IsHighlighter
            /// </summary>
            internal bool CachedIsHighlighter
            {
                get {return _cachedIsHighlighter;}
                set {_cachedIsHighlighter = value;}
            }
 
            /// <summary>
            /// The previous value of Color
            /// </summary>
            internal Color CachedColor
            {
                get {return _cachedColor;}
                set {_cachedColor = value;}
            }
 
            private Stroke                      _stroke;
            private bool                        _cachedIsHighlighter;
            private Color                       _cachedColor;
            private Renderer                    _renderer;
}
 
        /// <summary>
        /// Private helper that helps reverse map the highlighter dictionary
        /// </summary>
        private class HighlighterContainerVisual : ContainerVisual
        {
            internal HighlighterContainerVisual(Color color)
            {
                _color = color;
            }
 
            /// <summary>
            /// The Color of the strokes in this highlighter container visual
            /// </summary>
            internal Color Color
            {
                get { return _color; }
            }
            private Color _color;
        }
 
        #endregion
 
        #region Internal interface
 
        /// <summary>
        /// Public Constructor
        /// </summary>
        internal Renderer()
        {
            // Initialize the data members.
            // We intentionally don't use lazy initialization for the core members to avoid
            // hidden bug situations when one thing has been created while another not.
            // If the user is looking for lazy initialization, she should do that on her
            // own and create Renderer only when there's a need for it.
 
            // Create visuals that'll be the containers for all other visuals
            // created by the Renderer. This visuals are created once and are
            // not supposed to be replaced nor destroyed while the Renderer is alive.
            _rootVisual = new ContainerVisual();
            _highlightersRoot = new ContainerVisual();
            _regularInkVisuals = new ContainerVisual();
            _incrementalRenderingVisuals = new ContainerVisual();
 
            // Highlighters go to the bottom, then regular ink, and regular
            // ink' incremental rendering in on top.
            VisualCollection rootChildren = _rootVisual.Children;
            rootChildren.Add(_highlightersRoot);
            rootChildren.Add(_regularInkVisuals);
            rootChildren.Add(_incrementalRenderingVisuals);
 
            // Set the default value of highcontrast to be false.
            _highContrast = false;
 
            // Create a stroke-visual dictionary
            _visuals = new Dictionary<Stroke, StrokeVisual>();
        }
 
        /// <summary>
        /// Returns a reference to a visual tree that can be used to render the ink.
        /// This property may be either a single visual or a container visual with
        /// children. The element uses this visual as a child visual and arranges
        /// it with respects to the other siblings.
        /// Note: No visuals are actually generated until the application gets
        /// this property for the first time. If no strokes are set then an empty
        /// visual is returned.
        /// </summary>
        internal Visual RootVisual { get { return _rootVisual; } }
 
        /// <summary>
        /// Set the strokes property to the collection of strokes to be rendered.
        /// The Renderer will then listen to changes to the StrokeCollection
        /// and update its state to reflect the changes.
        /// </summary>
        internal StrokeCollection Strokes
        {
            get
            {
                // We should never return a null value.
                if ( _strokes == null )
                {
                    _strokes = new StrokeCollection();
 
                    // Start listening on events from the stroke collection.
                    _strokes.StrokesChangedInternal += new StrokeCollectionChangedEventHandler(OnStrokesChanged);
                }
 
                return _strokes;
            }
            set
            {
                ArgumentNullException.ThrowIfNull(value);
                if (value == _strokes)
                {
                    return;
                }
 
                // Detach the current stroke collection
                if (null != _strokes)
                {
                    // Stop listening on events from the stroke collection.
                    _strokes.StrokesChangedInternal -= new StrokeCollectionChangedEventHandler(OnStrokesChanged);
 
                    foreach (StrokeVisual visual in _visuals.Values)
                    {
                        StopListeningOnStrokeEvents(visual.Stroke);
                        // Detach the visual from the tree
                        DetachVisual(visual);
                    }
                    _visuals.Clear();
                }
 
                // Set it.
                _strokes = value;
 
                // Create visuals
                foreach (Stroke stroke in _strokes)
                {
                    // Create a visual per stroke
                    StrokeVisual visual = new StrokeVisual(stroke, this);
 
                    // Store the stroke-visual pair in the dictionary
                    _visuals.Add(stroke, visual);
 
                    StartListeningOnStrokeEvents(visual.Stroke);
 
                    // Attach it to the visual tree
                    AttachVisual(visual, true/*buildingStrokeCollection*/);
                }
 
                // Start listening on events from the stroke collection.
                _strokes.StrokesChangedInternal += new StrokeCollectionChangedEventHandler(OnStrokesChanged);
            }
        }
 
        /// <summary>
        /// User supposed to use this method to attach IncrementalRenderer's root visual
        /// to the visual tree of a stroke collection view.
        /// </summary>
        /// <param name="visual">visual to attach</param>
        /// <param name="drawingAttributes">drawing attributes that used in the incremental rendering</param>
        internal void AttachIncrementalRendering(Visual visual, DrawingAttributes drawingAttributes)
        {
            // Check the input parameters
            ArgumentNullException.ThrowIfNull(visual);
            ArgumentNullException.ThrowIfNull(drawingAttributes);
 
            //harden against eaten exceptions
            bool exceptionRaised = false;
 
            // Verify that the visual hasn't been attached already
            if (_attachedVisuals != null)
            {
                foreach(Visual alreadyAttachedVisual in _attachedVisuals)
                {
                    if (visual == alreadyAttachedVisual)
                    {
                        exceptionRaised = true;
                        throw new System.InvalidOperationException(SR.CannotAttachVisualTwice);
                    }
                }
            }
            else
            {
                // Create the list to register attached visuals in
                _attachedVisuals = new List<Visual>();
            }
 
 
            if (!exceptionRaised)
            {
                // The position of the visual in the tree depends on the drawingAttributes
                // Find the appropriate parent visual to attach this visual to.
                ContainerVisual parent = drawingAttributes.IsHighlighter ? GetContainerVisual(drawingAttributes) : _incrementalRenderingVisuals;
 
                // Attach the visual to the tree
                parent.Children.Add(visual);
 
                // Put the visual into the list of visuals attached via this method
                _attachedVisuals.Add(visual);
            }
        }
 
        /// <summary>
        /// Detaches a visual previously attached via AttachIncrementalRendering
        /// </summary>
        /// <param name="visual">the visual to detach</param>
        internal void DetachIncrementalRendering(Visual visual)
        {
            ArgumentNullException.ThrowIfNull(visual);
 
            // Remove the visual in the list of attached via AttachIncrementalRendering
            if ((_attachedVisuals == null) || (_attachedVisuals.Remove(visual) == false))
            {
                throw new System.InvalidOperationException(SR.VisualCannotBeDetached);
            }
 
            // Detach it from the tree
            DetachVisual(visual);
        }
 
        /// <summary>
        /// Internal helper used to indicate if a visual was previously attached 
        /// via a call to AttachIncrementalRendering
        /// </summary>
        internal bool ContainsAttachedIncrementalRenderingVisual(Visual visual)
        {
            if (visual == null || _attachedVisuals == null)
            {
                return false;
            }
 
            return _attachedVisuals.Contains(visual);
        }
 
        /// <summary>
        /// Internal helper used to determine if a visual is in the right spot in the visual tree
        /// </summary>
        internal bool AttachedVisualIsPositionedCorrectly(Visual visual, DrawingAttributes drawingAttributes)
        {
            if (visual == null || drawingAttributes == null || _attachedVisuals == null || !_attachedVisuals.Contains(visual))
            {
                return false;
            }
 
            ContainerVisual correctParent 
                = drawingAttributes.IsHighlighter ? GetContainerVisual(drawingAttributes) : _incrementalRenderingVisuals;
 
            ContainerVisual currentParent 
                = VisualTreeHelper.GetParent(visual) as ContainerVisual;
 
            if (currentParent == null || correctParent != currentParent)
            {
                return false;
            }
            return true;
        }
 
 
 
        /// <summary>
        /// TurnOnHighContrast turns on the HighContrast rendering mode
        /// </summary>
        /// <param name="strokeColor">The stroke color under high contrast</param>
        internal void TurnHighContrastOn(Color strokeColor)
        {
            if ( !_highContrast || strokeColor != _highContrastColor )
            {
                _highContrast = true;
                _highContrastColor = strokeColor;
                UpdateStrokeVisuals();
            }
        }
 
        /// <summary>
        /// ResetHighContrast turns off the HighContrast mode
        /// </summary>
        internal void TurnHighContrastOff()
        {
            if ( _highContrast )
            {
                _highContrast = false;
                UpdateStrokeVisuals();
            }
        }
 
        /// <summary>
        /// Indicates whether the renderer is in high contrast mode.
        /// </summary>
        /// <returns></returns>
        internal bool IsHighContrast()
        {
            return _highContrast;
        }
 
        /// <summary>
        /// returns the stroke color for the high contrast rendering.
        /// </summary>
        /// <returns></returns>
        public Color GetHighContrastColor()
        {
            return _highContrastColor;
        }
 
        #endregion
 
        #region Event handlers
 
        /// <summary>
        /// StrokeCollectionChanged event handler
        /// </summary>
        private void OnStrokesChanged(object sender, StrokeCollectionChangedEventArgs eventArgs)
        {
            System.Diagnostics.Debug.Assert(sender == _strokes);
 
            // Read the args
            StrokeCollection added = eventArgs.Added;
            StrokeCollection removed = eventArgs.Removed;
 
            // Add new strokes
            foreach (Stroke stroke in added)
            {
                // Verify that it's not a dupe
                if (_visuals.ContainsKey(stroke))
                {
                    throw new System.ArgumentException(SR.DuplicateStrokeAdded);
                }
 
                // Create a visual for the new stroke and add it to the dictionary
                StrokeVisual visual = new StrokeVisual(stroke, this);
                _visuals.Add(stroke, visual);
 
                // Start listening on the stroke events
                StartListeningOnStrokeEvents(visual.Stroke);
 
                // Attach it to the visual tree
                AttachVisual(visual, false/*buildingStrokeCollection*/);
            }
 
            // Deal with removed strokes first
            foreach (Stroke stroke in removed)
            {
                // Verify that the event is in sync with the view
                StrokeVisual visual = null;
                if (_visuals.TryGetValue(stroke, out visual))
                {
                    // get rid of both the visual and the stroke
                    DetachVisual(visual);
                    StopListeningOnStrokeEvents(visual.Stroke);
                    _visuals.Remove(stroke);
                }
                else
                {
                    throw new System.ArgumentException(SR.UnknownStroke3);
                }
            }
        }
 
        /// <summary>
        /// Stroke Invalidated event handler
        /// </summary>
        private void OnStrokeInvalidated(object sender, EventArgs eventArgs)
        {
            System.Diagnostics.Debug.Assert(_strokes.IndexOf(sender as Stroke) != -1);
 
            // Find the visual associated with the changed stroke.
            StrokeVisual visual;
            Stroke stroke = (Stroke)sender;
            if (_visuals.TryGetValue(stroke, out visual) == false)
            {
                throw new System.ArgumentException(SR.UnknownStroke1);
            }
 
            // The original value of IsHighligher and Color are cached in StrokeVisual.
            // if (IsHighlighter value changed or (IsHighlighter == true and not changed and color changed)
            // detach and re-attach the corresponding visual;
            // otherwise Invalidate the corresponding StrokeVisual
            if (visual.CachedIsHighlighter != stroke.DrawingAttributes.IsHighlighter ||
                (stroke.DrawingAttributes.IsHighlighter &&
                    StrokeRenderer.GetHighlighterColor(visual.CachedColor) != StrokeRenderer.GetHighlighterColor(stroke.DrawingAttributes.Color)))
            {
                // The change requires reparenting the visual in the tree.
                DetachVisual(visual);
                AttachVisual(visual, false/*buildingStrokeCollection*/);
 
                // Update the cached values
                visual.CachedIsHighlighter = stroke.DrawingAttributes.IsHighlighter;
                visual.CachedColor = stroke.DrawingAttributes.Color;
            }
            // Update the visual.
            visual.Update();
        }
 
        #endregion
 
        #region Helper methods
 
        /// <summary>
        /// Update the stroke visuals
        /// </summary>
        private void UpdateStrokeVisuals()
        {
            foreach ( StrokeVisual strokeVisual in _visuals.Values )
            {
                strokeVisual.Update();
            }
        }
 
        /// <summary>
        /// Attaches a stroke visual to the tree based on the stroke's
        /// drawing attributes and/or its z-order (index in the collection).
        /// </summary>
        private void AttachVisual(StrokeVisual visual, bool buildingStrokeCollection)
        {
            System.Diagnostics.Debug.Assert(_strokes != null);
 
            if (visual.Stroke.DrawingAttributes.IsHighlighter)
            {
                // Find or create a container visual for highlighter strokes of the color
                ContainerVisual parent = GetContainerVisual(visual.Stroke.DrawingAttributes);
                Debug.Assert(visual is StrokeVisual);
 
                //insert StrokeVisuals under any non-StrokeVisuals used for dynamic inking
                int i = 0;
                for (int j = parent.Children.Count - 1; j >= 0; j--)
                {
                    if (parent.Children[j] is StrokeVisual)
                    {
                        i = j + 1;
                        break;
                    }
                }
                parent.Children.Insert(i, visual);
            }
            else
            {
                // For regular ink we have to respect the z-order of the strokes.
                // The implementation below is not optimal in a generic case, but the
                // most simple and should work ok in most common scenarios.
 
                // Find the nearest non-highlighter stroke with a lower z-order
                // and insert the new visual right next to the visual of that stroke.
                StrokeVisual precedingVisual = null;
                int i = 0;
                if (buildingStrokeCollection)
                {
                    Stroke visualStroke = visual.Stroke;
                    //we're building up a stroke collection, no need to start at IndexOf,
                    i = Math.Min(_visuals.Count, _strokes.Count); //not -1, we're about to decrement
                    while (--i >= 0)
                    {
                        if (object.ReferenceEquals(_strokes[i], visualStroke))
                        {
                            break;
                        }
                    }
                }
                else
                {
                    i = _strokes.IndexOf(visual.Stroke);
                }
                while (--i >= 0)
                {
                    Stroke stroke = _strokes[i];
                    if ((stroke.DrawingAttributes.IsHighlighter == false)
                        && (_visuals.TryGetValue(stroke, out precedingVisual) == true)
                        && (VisualTreeHelper.GetParent(precedingVisual) != null))
                    {
                        VisualCollection children = ((ContainerVisual)(VisualTreeHelper.GetParent(precedingVisual))).Children;
                        int index = children.IndexOf(precedingVisual);
                        children.Insert(index + 1, visual);
                        break;
                    }
                }
                // If found no non-highlighter strokes with a lower z-order, insert
                // the stroke at the very bottom of the regular ink visual tree.
                if (i < 0)
                {
                    ContainerVisual parent = GetContainerVisual(visual.Stroke.DrawingAttributes);
                    parent.Children.Insert(0, visual);
                }
            }
        }
 
        /// <summary>
        /// Detaches a visual from the tree, also removes highligher parents if empty
        /// when true is passed
        /// </summary>
        private void DetachVisual(Visual visual)
        {
            ContainerVisual parent = (ContainerVisual)(VisualTreeHelper.GetParent(visual));
            if (parent != null)
            {
                VisualCollection children = parent.Children;
                children.Remove(visual);
 
                // If the parent is a childless highlighter, detach it too.
                HighlighterContainerVisual hcVisual = parent as HighlighterContainerVisual;
                if (hcVisual != null &&
                    hcVisual.Children.Count == 0 &&
                    _highlighters != null &&
                    _highlighters.ContainsValue(hcVisual))
                {
                    DetachVisual(hcVisual);
                    _highlighters.Remove(hcVisual.Color);
                }
            }
        }
 
        /// <summary>
        /// Attaches event handlers to stroke events
        /// </summary>
        private void StartListeningOnStrokeEvents(Stroke stroke)
        {
            System.Diagnostics.Debug.Assert(stroke != null);
            stroke.Invalidated += new EventHandler(OnStrokeInvalidated);
        }
 
        /// <summary>
        /// Detaches event handlers from stroke
        /// </summary>
        private void StopListeningOnStrokeEvents(Stroke stroke)
        {
            System.Diagnostics.Debug.Assert(stroke != null);
            stroke.Invalidated -= new EventHandler(OnStrokeInvalidated);
        }
 
        /// <summary>
        /// Finds a container for a new visual based on the drawing attributes
        /// of the stroke rendered into that visual.
        /// </summary>
        /// <param name="drawingAttributes">drawing attributes</param>
        /// <returns>visual</returns>
        private ContainerVisual GetContainerVisual(DrawingAttributes drawingAttributes)
        {
            System.Diagnostics.Debug.Assert(drawingAttributes != null);
 
            HighlighterContainerVisual hcVisual;
            if (drawingAttributes.IsHighlighter)
            {
                // For a highlighter stroke, the color.A is neglected.
                Color color = StrokeRenderer.GetHighlighterColor(drawingAttributes.Color);
                if ((_highlighters == null) || (_highlighters.TryGetValue(color, out hcVisual) == false))
                {
                    if (_highlighters == null)
                    {
                        _highlighters = new Dictionary<Color, HighlighterContainerVisual>();
                    }
 
                    hcVisual = new HighlighterContainerVisual(color);
                    hcVisual.Opacity = StrokeRenderer.HighlighterOpacity;
                    _highlightersRoot.Children.Add(hcVisual);
 
                    _highlighters.Add(color, hcVisual);
                }
                else if (VisualTreeHelper.GetParent(hcVisual) == null)
                {
                    _highlightersRoot.Children.Add(hcVisual);
                }
                return hcVisual;
            }
            else
            {
                return _regularInkVisuals;
            }
        }
 
        #endregion
 
        #region Fields
 
        // The renderer's top level container visuals
        private ContainerVisual _rootVisual;
        private ContainerVisual _highlightersRoot;
        private ContainerVisual _incrementalRenderingVisuals;
        private ContainerVisual _regularInkVisuals;
 
        // Stroke-to-visual map
        private Dictionary<Stroke, StrokeVisual> _visuals;
 
        // Color-to-visual map for highlighter ink container visuals
        private Dictionary<Color, HighlighterContainerVisual> _highlighters = null;
 
        // Collection of strokes this Renderer renders
        private StrokeCollection _strokes = null;
 
        // List of visuals attached via AttachIncrementalRendering
        private List<Visual> _attachedVisuals = null;
 
        // When true, will render in high contrast mode
        private bool _highContrast;
        private Color _highContrastColor = Colors.White;
 
        #endregion
    }
}