File: src\Framework\MS\Internal\Text\Line.cs
Project: wpf\PresentationFramework.csproj (PresentationFramework)
//---------------------------------------------------------------------------
//
// Copyright (C) Microsoft Corporation.  All rights reserved.
//
// File: Line.cs
//
// Description: Text line formatter.
//
// History:
//  04/25/2003 : Microsoft - moving from Avalon branch.
//
//---------------------------------------------------------------------------
 
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using MS.Internal.PtsHost;
 
namespace MS.Internal.Text
{
    // ----------------------------------------------------------------------
    // Text line formatter.
    // ----------------------------------------------------------------------
    internal abstract class Line : TextSource, IDisposable
    {
        // ------------------------------------------------------------------
        //
        //  IDisposable Implementation
        //
        // ------------------------------------------------------------------
 
        #region IDisposable Implementation
 
        // ------------------------------------------------------------------
        // Free all resources associated with the line. Prepare it for reuse.
        // ------------------------------------------------------------------
        public void Dispose()
        {
            // Dispose text line
            if (_line != null)
            {
                _line.Dispose();
                _line = null;
            }
            GC.SuppressFinalize(this);
        }
 
        #endregion IDisposable Implementation
 
        //-------------------------------------------------------------------
        //
        //  Internal Methods
        //
        //-------------------------------------------------------------------
 
        #region Internal Methods
 
        // ------------------------------------------------------------------
        // Constructor.
        //
        //      owner - owner of the line.
        // ------------------------------------------------------------------
        internal Line(System.Windows.Controls.TextBlock owner)
        {
            _owner = owner;
            _textAlignment = owner.TextAlignment;
            _showParagraphEllipsis = false;
            _wrappingWidth = _owner.RenderSize.Width;
            PixelsPerDip = _owner.GetDpi().PixelsPerDip;
        }
 
        // ------------------------------------------------------------------
        // Create and format text line.
        //
        //      lineStartIndex - index of the first character in the line
        //      width - wrapping width of the line
        //      lineProperties - properties of the line
        //      textRunCache - run cache used by text formatter
        //      showParagraphEllipsis - true if paragraph ellipsis is shown 
        //                              at the end of the line
        // ------------------------------------------------------------------
        internal void Format(int dcp, double width, TextParagraphProperties lineProperties, TextLineBreak textLineBreak, TextRunCache textRunCache, bool showParagraphEllipsis)
        {
#if TEXTPANELLAYOUTDEBUG
            TextPanelDebug.IncrementCounter("Line.Format", TextPanelDebug.Category.TextView);
#endif
            _mirror = (lineProperties.FlowDirection == FlowDirection.RightToLeft);
            _dcp = dcp;
            _showParagraphEllipsis = showParagraphEllipsis;
            _wrappingWidth = width;
            _line = _owner.TextFormatter.FormatLine(this, dcp, width, lineProperties, textLineBreak, textRunCache);
        }
 
        // ------------------------------------------------------------------
        // Arrange content of formatted line.
        //
        //      vc - Visual collection of the parent.
        //      lineOffset - Offset of the line.
        // ------------------------------------------------------------------
        internal virtual void Arrange(VisualCollection vc, Vector lineOffset)
        {
        }
 
        // ------------------------------------------------------------------
        // Render formatted line.
        //
        //      ctx - Drawing context to be used for rendering.
        //      lineOffset - Offset of the line.
        //      wrappingWidth - Wrapping width for the line.
        // ------------------------------------------------------------------
        internal void Render(DrawingContext ctx, Point lineOffset)
        {
            Debug.Assert(_line != null, "Rendering line that has not been measured yet.");
 
            // Handle text trimming.
            System.Windows.Media.TextFormatting.TextLine line = _line;
            if (_line.HasOverflowed && _owner.ParagraphProperties.TextTrimming != TextTrimming.None)
            {
                line = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
                Debug.Assert(line.HasCollapsed, "Line has not been collapsed");
            }
 
            double delta = CalculateXOffsetShift();
            line.Draw(ctx, new Point(lineOffset.X + delta, lineOffset.Y), (_mirror ? InvertAxes.Horizontal : InvertAxes.None));
        }
 
        // ------------------------------------------------------------------
        // Retrieve bounds of an object/character at specified text position.
        //
        //      characterIndex - position of an object/character
        //      flowDirection - flow direction of object/character
        //
        // Returns: Bounds of an object/character.
        // ------------------------------------------------------------------
        internal Rect GetBoundsFromTextPosition(int characterIndex, out FlowDirection flowDirection)
        {
            return GetBoundsFromPosition(characterIndex, 1, out flowDirection);
        }
 
        /// <summary>
        /// Returns an ArrayList of rectangles (Rect) that form the bounds of the region specified between
        /// the start and end points
        /// </summary>
        /// <param name="cp"></param>
        /// int offset indicating the starting point of the region for which bounds are required
        /// <param name="cch">
        /// Length in characters of the region for which bounds are required
        /// </param>
        /// <param name="xOffset">
        /// Offset of line in x direction, to be added to line bounds to get actual rectangle for line
        /// </param>
        /// <param name="yOffset">
        /// Offset of line in y direction, to be added to line bounds to get actual rectangle for line
        /// </param>
        /// <remarks>
        /// This function calls GetTextBounds for the line, and then checks if there are text run bounds. If they exist,
        /// it uses those as the bounding rectangles. If not, it returns the rectangle for the first (and only) element
        /// of the text bounds.
        /// </remarks>
        internal List<Rect> GetRangeBounds(int cp, int cch, double xOffset, double yOffset)
        {
            List<Rect> rectangles = new List<Rect>();
            
            // Adjust x offset for trailing spaces
            double delta = CalculateXOffsetShift();
            double adjustedXOffset = xOffset + delta;
 
            IList<TextBounds> textBounds;
            if (_line.HasOverflowed && _owner.ParagraphProperties.TextTrimming != TextTrimming.None)
            {
                // We should not shift offset in this case
                Invariant.Assert(DoubleUtil.AreClose(delta, 0));
                System.Windows.Media.TextFormatting.TextLine line = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
                Invariant.Assert(line.HasCollapsed, "Line has not been collapsed");
                textBounds = line.GetTextBounds(cp, cch);
            }
            else
            {
                textBounds = _line.GetTextBounds(cp, cch);
            }
            Invariant.Assert(textBounds.Count > 0);
 
 
            for (int boundIndex = 0; boundIndex < textBounds.Count; boundIndex++)
            {
                Rect rect = textBounds[boundIndex].Rectangle;
                rect.X += adjustedXOffset;
                rect.Y += yOffset;
                rectangles.Add(rect);
            }
            return rectangles;
        }
 
        //-------------------------------------------------------------------
        // Retrieve text position index from the distance.
        //
        //      distance - distance relative to the beginning of the line
        //
        // Returns: Text position index.
        //-------------------------------------------------------------------
        internal CharacterHit GetTextPositionFromDistance(double distance)
        {
            // Adjust distance to account for a line shift due to rendering of trailing spaces
            double delta = CalculateXOffsetShift();
            if (_line.HasOverflowed && _owner.ParagraphProperties.TextTrimming != TextTrimming.None)
            {
                System.Windows.Media.TextFormatting.TextLine line = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
                Invariant.Assert(DoubleUtil.AreClose(delta, 0));
                Invariant.Assert(line.HasCollapsed, "Line has not been collapsed");
                return line.GetCharacterHitFromDistance(distance);
            }
            return _line.GetCharacterHitFromDistance(distance - delta);
        }
 
        //-------------------------------------------------------------------
        // Retrieve text position for next caret position
        //
        // index: CharacterHit for current position
        //
        // Returns: Text position index.
        //-------------------------------------------------------------------
        internal CharacterHit GetNextCaretCharacterHit(CharacterHit index)
        {
            return _line.GetNextCaretCharacterHit(index);
        }
 
        //-------------------------------------------------------------------
        // Retrieve text position for previous caret position
        //
        // index: CharacterHit for current position
        //
        // Returns: Text position index.
        //-------------------------------------------------------------------
        internal CharacterHit GetPreviousCaretCharacterHit(CharacterHit index)
        {
            return _line.GetPreviousCaretCharacterHit(index);
        }
 
        //-------------------------------------------------------------------
        // Retrieve text position for backspace caret position
        //
        // index: CharacterHit for current position
        //
        // Returns: Text position index.
        //-------------------------------------------------------------------
        internal CharacterHit GetBackspaceCaretCharacterHit(CharacterHit index)
        {
            return _line.GetBackspaceCaretCharacterHit(index);
        }
 
        /// <summary>
        /// Returns true of char hit is at caret unit boundary.
        /// </summary>
        /// <param name="charHit">
        /// CharacterHit to be tested.
        /// </param>
        internal bool IsAtCaretCharacterHit(CharacterHit charHit)
        {
            return _line.IsAtCaretCharacterHit(charHit, _dcp);
        }
 
        // ------------------------------------------------------------------
        // Find out if there are any inline objects.
        // ------------------------------------------------------------------
        internal virtual bool HasInlineObjects()
        {
            return false;
        }
 
        // ------------------------------------------------------------------
        //  Hit tests to the correct ContentElement within the line.
        //
        //      offset - offset within the line.
        //
        // Returns: ContentElement which has been hit.
        // ------------------------------------------------------------------
        internal virtual IInputElement InputHitTest(double offset)
        {
            return null;
        }
 
        /// <summary>
        /// Passes linebreak object back up from contained line
        /// </summary>
        internal TextLineBreak GetTextLineBreak()
        {
            if(_line == null)
            {
                return null;
            }
 
            return _line.GetTextLineBreak();
        }
 
        // ------------------------------------------------------------------
        // Get length of content hidden by ellipses.
        //
        //      wrappingWidth - Wrapping width for the line.
        //
        // Returns: Length of collapsed content (number of characters hidden
        //          by ellipses).
        // ------------------------------------------------------------------
        internal int GetEllipsesLength()
        {
            // There are no ellipses, if:
            // * there is no overflow in the line
            // * text trimming is turned off
            if (!_line.HasOverflowed) { return 0; }
            if (_owner.ParagraphProperties.TextTrimming == TextTrimming.None) { return 0; }
 
            // Create collapsed text line to get length of collapsed content.
            System.Windows.Media.TextFormatting.TextLine collapsedLine = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
            Debug.Assert(collapsedLine.HasCollapsed, "Line has not been collapsed");
            IList<TextCollapsedRange> collapsedRanges = collapsedLine.GetTextCollapsedRanges();
            if (collapsedRanges != null)
            {
                Debug.Assert(collapsedRanges.Count == 1, "Multiple collapsed ranges are not supported.");
                TextCollapsedRange collapsedRange = collapsedRanges[0];
                return collapsedRange.Length;
            }
            return 0;
        }
 
 
        // ------------------------------------------------------------------
        // Gets width of content, collapsed at wrappingWidth (if necessary)
        //
        //      wrappingWidth - Wrapping width for the line.
        //
        // Returns: Width of content, after collapse (may be greater than wrappingWidth)
        //
        // ------------------------------------------------------------------
        internal double GetCollapsedWidth()
        {
            // There are no ellipses, if:
            // * there is no overflow in the line
            // * text trimming is turned off
            if (!_line.HasOverflowed) 
            { 
                return Width; 
            }
            if (_owner.ParagraphProperties.TextTrimming == TextTrimming.None) 
            {
                return Width;
            }
 
            // Create collapsed text line to get length of collapsed content.
            System.Windows.Media.TextFormatting.TextLine collapsedLine = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
            Debug.Assert(collapsedLine.HasCollapsed, "Line has not been collapsed");
 
            return collapsedLine.Width;
        }
 
        #endregion Internal Methods
 
        //-------------------------------------------------------------------
        //
        //  Internal Properties
        //
        //-------------------------------------------------------------------
 
        #region Internal Properties
 
        // ------------------------------------------------------------------
        // Calculated width of the line.
        // ------------------------------------------------------------------
        internal double Width 
        { 
            get 
            {
                if (IsWidthAdjusted)
                {
                    // Trailing spaces add to width
                    return _line.WidthIncludingTrailingWhitespace;
                }
                else
                {
                    return _line.Width;
                }
            } 
        }
 
        // ------------------------------------------------------------------
        // Distance from the beginning of paragraph edge to the line edge.
        // ------------------------------------------------------------------
        internal double Start 
        { 
            get 
            {
                if (IsXOffsetAdjusted)
                {
                    return _line.Start + CalculateXOffsetShift();
                }
                else
                {
                    return _line.Start;
                }
            } 
        }
 
        // ------------------------------------------------------------------
        // Height of the line; line advance distance.
        // ------------------------------------------------------------------
        internal double Height { get { return _line.Height; } }
 
        // ------------------------------------------------------------------
        // Distance from top to baseline of this text line.
        // ------------------------------------------------------------------
        internal double BaselineOffset { get { return _line.Baseline; } }
 
        // ------------------------------------------------------------------
        // Is this the last line of the paragraph?
        // ------------------------------------------------------------------
        internal bool EndOfParagraph
        {
            get
            {
                // If there are no Newline characters, it is not the end of paragraph.
                if (_line.NewlineLength == 0) { return false; }
                // Since there are Newline characters in the line, do more expensive and
                // accurate check.
                IList<TextSpan<TextRun>> runs = _line.GetTextRunSpans();
                return (((TextSpan<TextRun>)runs[runs.Count-1]).Value is TextEndOfParagraph);
            }
        }
 
        // ------------------------------------------------------------------
        // Length of the line excluding any synthetic characters.
        // ------------------------------------------------------------------
        internal int Length { get { return _line.Length - (EndOfParagraph ? _syntheticCharacterLength : 0); } }
 
        // ------------------------------------------------------------------
        // Length of the line excluding any synthetic characters and line breaks.
        // ------------------------------------------------------------------
        internal int ContentLength { get { return _line.Length - _line.NewlineLength; } }
 
        #endregion Internal Properties
 
        //-------------------------------------------------------------------
        //
        //  Protected Methods
        //
        //-------------------------------------------------------------------
 
        #region Protected Methods
 
        // ------------------------------------------------------------------
        // Retrieve bounds of an object/character at specified text index.
        //
        //      cp - character index of an object/character
        //      cch - number of positions occupied by object/character
        //      flowDirection - flow direction of object/character
        //
        // Returns: Bounds of an object/character.
        // ------------------------------------------------------------------
        protected Rect GetBoundsFromPosition(int cp, int cch, out FlowDirection flowDirection)
        {
            Rect rect;
            
            // Adjust x offset for trailing spaces
            double delta = CalculateXOffsetShift();
            IList<TextBounds> textBounds;
            if (_line.HasOverflowed && _owner.ParagraphProperties.TextTrimming != TextTrimming.None)
            {
                // We should not shift offset in this case
                Invariant.Assert(DoubleUtil.AreClose(delta, 0));
                System.Windows.Media.TextFormatting.TextLine line = _line.Collapse(GetCollapsingProps(_wrappingWidth, _owner.ParagraphProperties));
                Invariant.Assert(line.HasCollapsed, "Line has not been collapsed");
                textBounds = line.GetTextBounds(cp, cch);
            }
            else
            {
                textBounds = _line.GetTextBounds(cp, cch);
            }
            Invariant.Assert(textBounds != null && textBounds.Count == 1, "Expecting exactly one TextBounds for a single text position.");
            
            IList<TextRunBounds> runBounds = textBounds[0].TextRunBounds;            
            if (runBounds != null)
            {
                Debug.Assert(runBounds.Count == 1, "Expecting exactly one TextRunBounds for a single text position.");
                rect = runBounds[0].Rectangle;
            }
            else
            {
                rect = textBounds[0].Rectangle;
            }
 
            rect.X += delta;
            flowDirection = textBounds[0].FlowDirection;
            return rect;
        }
 
        // ------------------------------------------------------------------
        // Get collapsing properties.
        //
        //      wrappingWidth - wrapping width for collapsed line.
        //      paraProperties - paragraph properties.
        //
        // Returns: Line collapsing properties.
        // ------------------------------------------------------------------
        protected TextCollapsingProperties GetCollapsingProps(double wrappingWidth, LineProperties paraProperties)
        {
            Debug.Assert(paraProperties.TextTrimming != TextTrimming.None, "Text trimming must be enabled.");
            TextCollapsingProperties collapsingProps;
            if (paraProperties.TextTrimming == TextTrimming.CharacterEllipsis)
            {
                collapsingProps = new TextTrailingCharacterEllipsis(wrappingWidth, paraProperties.DefaultTextRunProperties);
            }
            else
            {
                collapsingProps = new TextTrailingWordEllipsis(wrappingWidth, paraProperties.DefaultTextRunProperties);
            }
 
            return collapsingProps;
        }
 
        /// <summary>
        /// Returns amount of shift for X-offset to render trailing spaces
        /// </summary>
        protected double CalculateXOffsetShift()
        {
            // Assert that textblock autosize is working correctly and that moving the offset back
            // will not result in the front of the line being taken off rendered area
            if (IsXOffsetAdjusted)
            {
                if (_textAlignment == TextAlignment.Center)
                {
                    // Return trailing spaces length divided by two so line remains centered
                    return (_line.Width - _line.WidthIncludingTrailingWhitespace) / 2;
                }
                else
                {
                    return (_line.Width - _line.WidthIncludingTrailingWhitespace);
                }
            }
            else
            {
                return 0.0;
            }
        }
 
        #endregion Protected Methods
 
        //-------------------------------------------------------------------
        //
        //  Protected Properites
        //
        //-------------------------------------------------------------------
 
        #region Protected Properties
 
        /// <summary>
        /// True if eliipsis is displayed in the line
        /// </summary>
        protected bool ShowEllipsis
        {
            get
            { 
                if (_owner.ParagraphProperties.TextTrimming == TextTrimming.None)
                {
                    return false;
                }
                if (_line.HasOverflowed || _showParagraphEllipsis)
                {
                    return true;
                }
                return false;
            }
        }
 
        /// <summary>
        /// True if line ends in hard line break
        /// </summary>
        protected bool HasLineBreak
        {
            get
            {
                return (_line.NewlineLength > 0);
            }
        }
 
        /// <summary>
        /// True if line's X-offset needs adjustment to render trailing spaces
        /// </summary>
        protected bool IsXOffsetAdjusted
        {
            get
            {
                return ((_textAlignment == TextAlignment.Right || _textAlignment == TextAlignment.Center) && IsWidthAdjusted);
            }
        }
 
        /// <summary>
        /// True if line's width is adjusted to include trailing spaces. For right and center alignment we need to
        /// adjust line offset as well, but for left alignment we need to only make a width asjustment
        /// </summary>
        protected bool IsWidthAdjusted
        {
            get
            {
                bool adjusted = false;
 
                // Trailing spaces rendered only around hard breaks
                if (HasLineBreak || EndOfParagraph)
                {
                    // Lines with ellipsis are not shifted because ellipsis would not appear after trailing spaces
                    if (!ShowEllipsis)
                    {
                        adjusted = true;
                    }
                }
                return adjusted;
            }
        }
        
        #endregion Protected Properties
 
 
        //-------------------------------------------------------------------
        //
        //  Private Fields
        //
        //-------------------------------------------------------------------
 
        #region Private Fields
 
        // ------------------------------------------------------------------
        // Owner of the line.
        // ------------------------------------------------------------------
        protected System.Windows.Controls.TextBlock _owner;
 
        // ------------------------------------------------------------------
        // Cached text line.
        // ------------------------------------------------------------------
        protected System.Windows.Media.TextFormatting.TextLine _line;
 
        // ------------------------------------------------------------------
        // Index of the first character in the line.
        // ------------------------------------------------------------------
        protected int _dcp;
 
        // ------------------------------------------------------------------
        // Synthetic character length.
        // ------------------------------------------------------------------
        protected static int _syntheticCharacterLength = 1;
 
        // ------------------------------------------------------------------
        // Is text mirrored?
        // ------------------------------------------------------------------
        protected bool _mirror;
 
        /// <summary>
        ///  Alignment direction of line. Set during formatting.
        /// </summary>
        protected TextAlignment _textAlignment;
 
        /// <summary>
        /// Does the line habe paragraph ellipsis. This is determined during formatting depending upon
        /// the type of line properties passed.
        /// </summary>
        protected bool _showParagraphEllipsis;
 
        /// <summary>
        /// Wrapping width of line
        /// </summary>
        protected double _wrappingWidth;
 
        #endregion Private Fields
    }
}