File: Core\CSharp\MS\Internal\TextFormatting\FullTextState.cs
Project: wpf\src\PresentationCore.csproj (PresentationCore)
//-----------------------------------------------------------------------
//
//  Microsoft Windows Client Platform
//  Copyright (C) Microsoft Corporation
//
//  File:      FullTextState.cs
//
//  Contents:  Formatting state of full text
//
//  Created:   2-25-2004 Worachai Chaoweeraprasit (wchao)
//  History:   1-29-2005 (wchao) refactor drawing state out, keep full text
//             only for formatting state
//
//------------------------------------------------------------------------
 
 
using System;
using System.Security;
using System.Globalization;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.TextFormatting;
using MS.Internal.Shaping;
using MS.Internal.Generic;
 
 
namespace MS.Internal.TextFormatting
{
    /// <summary>
    /// Formatting state of full text
    /// </summary>
    internal sealed class FullTextState
    {
        private TextStore           _store;                     // formatting store for main text
        private TextStore           _markerStore;               // store specifically for marker
        private StatusFlags         _statusFlags;               // status flags
        private int                 _cpMeasured;                // number of CP successfully measured and fit within column width
        private int                 _lscpHyphenationLookAhead;  // LSCP after the last character examined by the hyphenation code.
        private bool                _isSideways;
 
 
        [Flags]
        private enum StatusFlags
        {
            None            = 0,
            VerticalAdjust  = 0x00000001,   // vertical adjustment on some runs required
            ForceWrap       = 0x00000002,   // force wrap
            KeepState       = 0x00000040,   // state should be kept in the line
        }
 
 
        /// <summary>
        /// A word smaller than seven characters does not require hyphenation. This is based on 
        /// observation in real Western books and publications. Advanced research on readability
        /// seems to support this finding. It suggests that the 'fovea' which is the center
        /// point of our vision, can only see three to four letters before and after the center
        /// of a word.
        /// </summary>
        /// <remarks>
        /// "It has been known for over 100 years that when we read, our eyes don't move smoothly 
        /// across the page, but rather make discrete jumps from word to word. We fixate on a word 
        /// for a period of time, roughly 200-250ms, then make a ballistic movement to another word. 
        /// These movements are called saccades and usually take 20-35ms. Most saccades are forward 
        /// movements from 7 to 9 letters.
        /// ...
        /// During a single fixation, there is a limit to the amount of information that can be 
        /// recognized. The fovea, which is the clear center point of our vision, can only see 
        /// three to four letters to the left and right of fixation at normal reading distances."
        /// 
        /// ["The Science of Word Recognition" by Kevin Larson; July 2004]
        /// </remarks>
        private const int MinCchWordToHyphenate = 7;
 
 
        /// <summary>
        /// Create a fulltext state object from formatting info for subsequent fulltext formatting
        /// e.g. fulltext line, mix/max computation, potential breakpoints for optimal paragraph formatting.
        /// </summary>
        internal static FullTextState Create(
            FormatSettings      settings,
            int                 cpFirst,
            int                 finiteFormatWidth
            )
        {
            // prepare text stores
            TextStore store = new TextStore(
                settings, 
                cpFirst, 
                0, 
                settings.GetFormatWidth(finiteFormatWidth)
                );
 
            ParaProp pap = settings.Pap;
            TextStore markerStore = null;
 
            if(     pap.FirstLineInParagraph
                &&  pap.TextMarkerProperties != null
                &&  pap.TextMarkerProperties.TextSource != null)
            {
                // create text store specifically for marker
                markerStore = new TextStore(
                    // create specialized settings for marker store e.g. with marker text source
                    new FormatSettings(
                        settings.Formatter,
                        pap.TextMarkerProperties.TextSource,
                        new TextRunCacheImp(),   // no cross-call run cache available for marker store
                        pap,                     // marker by default use the same paragraph properties
                        null,                    // not consider previousLineBreak
                        true,                    // isSingleLineFormatting
                        settings.TextFormattingMode,
                        settings.IsSideways
                        ),
                    0,                           // marker store always started with cp == 0
                    TextStore.LscpFirstMarker,   // first lscp value for marker text
                    Constants.IdealInfiniteWidth // formatWidth 
                    );
            }
 
            // construct a new fulltext state object
            return new FullTextState(store, markerStore, settings.IsSideways);
        }
 
 
        /// <summary>
        /// Construct full text state for formatting
        /// </summary>
        private FullTextState(
            TextStore       store,
            TextStore       markerStore,
            bool isSideways
            )
        {
            _isSideways = isSideways;
            _store = store;
            _markerStore = markerStore;
        }
 
 
        /// <summary>
        /// Number of client CP for which we know the nominal widths fit in the
        /// available margin (i.e., because we've measured them).
        /// </summary>
        internal int CpMeasured 
        {
            get 
            { 
                return _cpMeasured; 
            }
            set 
            { 
                _cpMeasured = value; 
            }
        }
 
 
        /// <summary>
        /// LSCP immediately after the last LSCP examined by hyphenation code while being run.
        /// This value is used to calculate a more precise DependentLength value for the line
        /// ended by automatic hyphenation. 
        /// </summary>
        internal int LscpHyphenationLookAhead
        {
            get
            {
                return _lscpHyphenationLookAhead;
            }
        }
 
        internal TextFormattingMode TextFormattingMode
        {
            get
            {
                return Formatter.TextFormattingMode;
            }
        }
 
        internal bool IsSideways
        {
            get
            {
                return _isSideways;
            }
        }
 
        /// <summary>
        /// Set tab stops
        /// </summary>
        /// <SecurityNote>
        /// Critical - as this calls TextFormatterContext.SetTabs which is Critical in four
        ///            different places.
        /// Safe - as the buffers are all created in the same function and the count
        ///        passed in the same as used for creating the buffer.
        /// </SecurityNote>
        [SecurityCritical, SecurityTreatAsSafe]
        internal void SetTabs(TextFormatterContext context)
        {
            unsafe
            {
                ParaProp pap = _store.Pap;
                FormatSettings settings = _store.Settings;
 
                // set up appropriate tab stops
                int incrementalTab = TextFormatterImp.RealToIdeal(pap.DefaultIncrementalTab);
                int lsTbdCount = pap.Tabs != null ? pap.Tabs.Count : 0;
                LsTbd[] lsTbds;
 
                if (_markerStore != null)
                {
                    if (pap.Tabs != null && pap.Tabs.Count > 0)
                    {
                        lsTbdCount = pap.Tabs.Count + 1;
                        lsTbds = new LsTbd[lsTbdCount];
                        lsTbds[0].ur = settings.TextIndent; // marker requires a tab stop at text start position
                        fixed (LsTbd* plsTbds = &lsTbds[1])
                        {
                            CreateLsTbds(pap, plsTbds, lsTbdCount - 1);
                            context.SetTabs(incrementalTab, plsTbds - 1, lsTbdCount);
                        }
                    }
                    else
                    {
                        LsTbd markerRequiredLsTbd = new LsTbd();
                        markerRequiredLsTbd.ur = settings.TextIndent; // marker requires a tab stop at text start position
                        context.SetTabs(incrementalTab, &markerRequiredLsTbd, 1);
                    }
                }
                else
                {
                    if (pap.Tabs != null && pap.Tabs.Count > 0)
                    {
                        lsTbds = new LsTbd[lsTbdCount];
                        fixed (LsTbd* plsTbds = &lsTbds[0])
                        {
                            CreateLsTbds(pap, plsTbds, lsTbdCount);
                            context.SetTabs(incrementalTab, plsTbds, lsTbdCount);
                        }
                    }
                    else
                    {
                        // work with only incremental tab
                        context.SetTabs(incrementalTab, null, 0);
                    }
                }
            }
        }
 
 
        /// <summary>
        /// Fill a fixed buffer of LsTbd with 
        /// </summary>
        /// <SecurityNote>
        /// Critical - This method writes into unmanaged array. 
        /// </SecurityNote>
        [SecurityCritical]
        private unsafe void CreateLsTbds(
            ParaProp        pap,
            LsTbd*          plsTbds,
            int             lsTbdCount
            )
        {
            for (int i = 0; i < lsTbdCount; i++)
            {
                TextTabProperties tab = (TextTabProperties)pap.Tabs[i];
                plsTbds[i].lskt = Convert.LsKTabFromTabAlignment(tab.Alignment);
                plsTbds[i].ur = TextFormatterImp.RealToIdeal(tab.Location);
 
                if (tab.TabLeader != 0)
                {
                    // Note: LS does not currently support surrogate character as tab leader and aligning character
                    plsTbds[i].wchTabLeader = (char)tab.TabLeader;
 
                    // tab leader requires state at display time for tab leader width fetching
                    _statusFlags |= StatusFlags.KeepState;
                }
                plsTbds[i].wchCharTab = (char)tab.AligningCharacter;
            }
        }
 
 
        /// <summary>
        /// Get distance from the start of main text to the end of marker
        /// </summary>
        /// <remarks>
        /// Positive distance is filtered out. Marker overlapping the main text is not supported.
        /// </remarks>
        internal int GetMainTextToMarkerIdealDistance()
        {
            if (_markerStore != null)
            {
                return Math.Min(0, TextFormatterImp.RealToIdeal(_markerStore.Pap.TextMarkerProperties.Offset) - _store.Settings.TextIndent);
            }
            return 0;
        }
 
        
        /// <summary>
        /// Map LSCP to host CP, and return the last LSRun
        /// before the specified limit.
        /// </summary>
        internal LSRun CountText(
            int         lscpLim,
            int         cpFirst,
            out int     count
            )
        {
            LSRun lastRun = null;
            count = 0;
 
            int lsccp = lscpLim - _store.CpFirst;
            Debug.Assert(lsccp > 0, "Zero-length text line!");
 
            foreach (Span span in _store.PlsrunVector)
            {
                if (lsccp <= 0)
                    break;
 
                Plsrun plsrun = (Plsrun)span.element;
 
                // There should be no marker runs in _plsrunVector.
                Debug.Assert(!TextStore.IsMarker(plsrun));
 
                // Is it a normal, non-static, LSRun?
                if (plsrun >= Plsrun.FormatAnchor)
                {
                    // Get the run and remember the last run.
                    lastRun = _store.GetRun(plsrun);
 
                    // Accumulate the length.
                    int cpRun = lastRun.Length;
                    if (cpRun > 0)
                    {
                        if (lsccp < span.length && cpRun == span.length)
                        {
                            count += lsccp;
                            break;
                        }
                        count += cpRun;
                    }
                }
 
                lsccp -= span.length;
            }
 
            // make char count relative to cpFirst as the cpFirst of this metrics may not
            // be the same as the cpFirst of the store in optimal paragraph formatting.
            count = count - cpFirst + _store.CpFirst;
 
            return lastRun;
        }
 
 
        /// <summary>
        /// Convert the specified external CP to LSCP corresponding to a possible break position for optimal break.
        /// </summary>
        /// <remarks>
        /// There is a generic issue that one CP could map to multiple LSCPs when it comes to
        /// lsrun that occupies no actual CP. Such lsrun is generated by line layout during
        /// formatting to accomplsih specific purpose i.e. run representing open and close
        /// reverse object or a fake-linebreak lsrun.
        /// 
        /// According to SergeyGe and Antons, LS will never breaks immediately after open reverse
        /// or immediately before close reverse. It also never break before a linebreak character. 
        ///
        /// Therefore, it is safe to make an assumption here that one CP will indeed map to one 
        /// LSCP given being the LSCP before the open reverse and the LSCP after the close reverse.
        /// Never the vice-versa.
        ///
        /// This is the reason why the loop inside this method may overread the PLSRUN span by
        /// one span at the end. The loop is designed to skip over all the lsruns with zero CP
        /// which is not the open reverse.
        /// 
        /// This logic is exactly the same as the one used by FullTextLine.PrefetchLSRuns. Any
        /// attempt to change it needs to be thoroughly reviewed. The same logic is to be applied
        /// accordingly on PrefetchLSRuns.
        /// 
        /// [Wchao, 5-24-2005]
        /// 
        /// </remarks>
        internal int GetBreakpointInternalCp(int cp)
        {
            int ccp = cp - _store.CpFirst;
            int lscp = _store.CpFirst;
            int ccpCurrent = 0;
 
            SpanVector plsrunVector = _store.PlsrunVector;
            LSRun lsrun;
            int i = 0;
 
            int lastSpanLength = 0;
            int lastRunLength = 0;
 
            do
            {
                Span span = plsrunVector[i];
                Plsrun plsrun = (Plsrun)span.element;
                lsrun = _store.GetRun(plsrun);
 
                if (ccp == ccpCurrent && lsrun.Type == Plsrun.Reverse)
                {
                    break;
                }
 
                lastSpanLength = span.length;
                lastRunLength = (plsrun >= Plsrun.FormatAnchor ? lsrun.Length : 0);
 
                lscp += lastSpanLength;
                ccpCurrent += lastRunLength;
 
            } while (   ++i < plsrunVector.Count
                    &&  lsrun.Type != Plsrun.ParaBreak
                    &&  ccp >= ccpCurrent
                );
 
            // Since we may overread the span vector by one span,
            // we need to subtract the accumulated lscp by the number of cp we may have overread.
 
            if (ccpCurrent == ccp || lastSpanLength == lastRunLength)
                return lscp - ccpCurrent + ccp;
 
            Invariant.Assert(ccpCurrent - ccp == lastRunLength);
            return lscp - lastSpanLength;
        }
 
 
        /// <summary>
        /// Find the hyphen break following or preceding the specified current LSCP
        /// </summary>
        /// <remarks>
        /// This method never checks whether the specified current LSCP is already right
        /// at a hyphen break. It either finds the next or the previous break regardless.
        /// 
        /// A negative lscchLim param value indicates the caller finds the hyphen immediately
        /// before the specified character index.
        /// </remarks>
        /// <param name="lscpCurrent">the current LSCP</param>
        /// <param name="lscchLim">the number of LSCP to search for break</param>
        /// <param name="isCurrentAtWordStart">flag indicates whether lscpCurrent is the beginning of the word to hyphenate</param>
        /// <param name="lscpHyphen">LSCP of the hyphen</param>
        /// <param name="lshyph">Hyphen properties</param>
        internal bool FindNextHyphenBreak(
            int         lscpCurrent,
            int         lscchLim,
            bool        isCurrentAtWordStart,
            ref int     lscpHyphen,
            ref LsHyph  lshyph
            )
        {
            lshyph = new LsHyph();  // no additional hyphen properties for now
 
            if (_store.Pap.Hyphenator != null)
            {
                int lscpChunk;
                int lscchChunk;
 
                LexicalChunk chunk = GetChunk(
                    _store.Pap.Hyphenator,
                    lscpCurrent,
                    lscchLim,
                    isCurrentAtWordStart,
                    out lscpChunk,
                    out lscchChunk
                    );
 
                _lscpHyphenationLookAhead = lscpChunk + lscchChunk;
 
                if (!chunk.IsNoBreak)
                {
                    int ichCurrent = chunk.LSCPToCharacterIndex(lscpCurrent - lscpChunk);
                    int ichLim = chunk.LSCPToCharacterIndex(lscpCurrent + lscchLim - lscpChunk);
 
                    if (lscchLim >= 0)
                    {
                        int ichNext = chunk.Breaks.GetNextBreak(ichCurrent);
 
                        if (ichNext >= 0 && ichNext > ichCurrent && ichNext <= ichLim)
                        {
                            // -1 because ichNext is the character index where break occurs in front of it,
                            // while LSCP is the position where break occurs after it. 
                            lscpHyphen = chunk.CharacterIndexToLSCP(ichNext - 1) + lscpChunk;
                            return true;
                        }
                    }
                    else
                    {
                        int ichPrev = chunk.Breaks.GetPreviousBreak(ichCurrent);
 
                        if (ichPrev >= 0 && ichPrev <= ichCurrent && ichPrev > ichLim)
                        {
                            // -1 because ichPrev is the character index where break occurs in front of it,
                            // while LSCP is the position where break occurs after it. 
                            lscpHyphen = chunk.CharacterIndexToLSCP(ichPrev - 1) + lscpChunk;
                            return true;
                        }
                    }
                }
            }
            return false;
        }
 
 
        /// <summary>
        /// Get the lexical chunk the specified current LSCP is within.
        /// </summary>
        private LexicalChunk GetChunk(
            TextLexicalService      lexicalService,
            int                     lscpCurrent,
            int                     lscchLim,
            bool                    isCurrentAtWordStart,
            out int                 lscpChunk,
            out int                 lscchChunk
            )
        {
            int lscpStart = lscpCurrent;
            int lscpLim = lscpStart + lscchLim;
 
            int cpFirst = _store.CpFirst;
 
            if (lscpStart > lscpLim)
            {
                // Start is always before limit
                lscpStart = lscpLim;
                lscpLim = lscpCurrent;
            }
 
            LexicalChunk chunk = new LexicalChunk();
            int cchWordMax;
            CultureInfo textCulture;
            SpanVector<int> textVector;
 
            char[] rawText = _store.CollectRawWord(
                lscpStart,
                isCurrentAtWordStart,
                _isSideways,
                out lscpChunk,
                out lscchChunk,
                out textCulture,
                out cchWordMax,
                out textVector
                );
 
            if (    rawText != null
                &&  cchWordMax >= MinCchWordToHyphenate
                &&  lscpLim < lscpChunk + lscchChunk
                &&  textCulture != null 
                &&  lexicalService != null
                &&  lexicalService.IsCultureSupported(textCulture)
                )
            {
                // analyze the chunk and produce the lexical chunk to cache
                TextLexicalBreaks breaks = lexicalService.AnalyzeText(
                    rawText,
                    rawText.Length,
                    textCulture
                    );
 
                if (breaks != null)
                {
                    chunk = new LexicalChunk(breaks, textVector);
                }
            }
 
            return chunk;
        }
 
 
        /// <summary>
        /// Get a text store containing the specified plsrun
        /// </summary>
        internal TextStore StoreFrom(Plsrun plsrun)
        {
            return TextStore.IsMarker(plsrun) ? _markerStore : _store;
        }
 
 
        /// <summary>
        /// Get a text store containing the specified lscp
        /// </summary>
        internal TextStore StoreFrom(int lscp)
        {
            return lscp < 0 ? _markerStore : _store;
        }
 
 
        /// <summary>
        /// Flag indicating whether vertical adjustment of some runs is required
        /// </summary>
        internal bool VerticalAdjust
        {
            get { return (_statusFlags & StatusFlags.VerticalAdjust) != 0; }
            set 
            {
                if (value)
                    _statusFlags |= StatusFlags.VerticalAdjust; 
                else
                    _statusFlags &= ~StatusFlags.VerticalAdjust;
            }
        }
 
 
        /// <summary>
        /// Flag indicating whether force wrap is required
        /// </summary>
        internal bool ForceWrap
        {
            get { return (_statusFlags & StatusFlags.ForceWrap) != 0; }
            set 
            {
                if (value)
                    _statusFlags |= StatusFlags.ForceWrap; 
                else
                    _statusFlags &= ~StatusFlags.ForceWrap;
            }
        }
 
 
        /// <summary>
        /// Flag indicating whether state should be kept in the line
        /// </summary>
        internal bool KeepState
        {
            get { return (_statusFlags & StatusFlags.KeepState) != 0; }
        }
 
 
        /// <summary>
        /// Formatting store for main text
        /// </summary>
        internal TextStore TextStore
        {
            get { return _store; }
        }
 
 
        /// <summary>
        /// Formatting store for marker text
        /// </summary>
        internal TextStore TextMarkerStore
        {
            get { return _markerStore; }
        }
 
 
        /// <summary>
        /// Current formatter
        /// </summary>
        internal TextFormatterImp Formatter
        {
            get { return _store.Settings.Formatter; }
        }
 
 
        /// <summary>
        /// formattng ideal width
        /// </summary>
        internal int FormatWidth
        {
            get { return _store.FormatWidth; }
        }
    }
}