Code:
/ Dotnetfx_Win7_3.5.1 / Dotnetfx_Win7_3.5.1 / 3.5.1 / DEVDIV / depot / DevDiv / releases / Orcas / NetFXw7 / wpf / src / Core / CSharp / MS / Internal / TextFormatting / FullTextLine.cs / 1 / FullTextLine.cs
//------------------------------------------------------------------------ // // Microsoft Windows Client Platform // Copyright (C) Microsoft Corporation, 2001 // // File: FullTextLine.cs // // Contents: Complex implementation of TextLine // // Created: 5-6-2002 Worachai Chaoweeraprasit (wchao) // //----------------------------------------------------------------------- using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Security; using System.Windows; using System.Windows.Media; using System.Windows.Media.TextFormatting; using MS.Internal; using MS.Internal.Shaping; using SR = MS.Internal.PresentationCore.SR; using SRID = MS.Internal.PresentationCore.SRID; namespace MS.Internal.TextFormatting { ////// Make FullTextLine nested type of TextMetrics to allow full access to TextMetrics private members /// internal partial struct TextMetrics : ITextMetrics { ////// Complex implementation of TextLine /// /// TextLine implementation around /// o Line Services /// o OpenType Services Library /// o Implementation of Unicode Bidirectional algorithm /// o Complex script itemizer /// o Composite font with generic glyph hunting algorithm /// internal class FullTextLine : TextLine { private TextMetrics _metrics; // Text metrics private int _cpFirst; // character index to the first charcter of the line private int _depthQueryMax; // maximum depth of reversals used in querying private int _paragraphWidth; // paragraph width private int _textMinWidthAtTrailing; // smallest text width excluding trailing whitespaces private SecurityCriticalDataForSet_ploline; // actual LS line private SecurityCriticalDataForSet _ploc; // actual LS context private Overhang _overhang; // overhang metrics private StatusFlags _statusFlags; // status flags of the line private SpanVector _plsrunVector; // plsrun span vector indexed by lscp private ArrayList _lsrunsMainText; // list of lsrun of main text private ArrayList _lsrunsMarkerText; // list of lsrun of marker text private FullTextState _fullText; // full text state kept for collapsing purpose (only have it when StatusFlags.HasOverflowed is set) private FormattedTextSymbols _collapsingSymbol; // line-end collapsing symbol private TextCollapsedRange _collapsedRange; // line-end collapsed range private TextSource _textSource; // Text Source of the main text for the line private TextDecorationCollection _paragraphTextDecorations; // Paragraph-level text decorations (or null if none) private Brush _defaultTextDecorationsBrush; // Default brush for paragraph text decorations [Flags] private enum StatusFlags { None = 0, IsDisposed = 0x00000001, HasOverflowed = 0x00000002, BoundingBoxComputed = 0x00000004, RightToLeft = 0x00000008, HasCollapsed = 0x00000010, KeepState = 0x00000020, IsTruncated = 0x00000040, } private enum CaretDirection { Forward, Backward, Backspace } /// /// Constructing a FullTextLine /// /// text formatting settings /// Line's first cp /// character length of the line /// paragraph width /// line formatting control flags internal FullTextLine( FormatSettings settings, int cpFirst, int lineLength, int paragraphWidth, LineFlags lineFlags ) : this() { if ( (lineFlags & LineFlags.KeepState) != 0 || settings.Pap.AlwaysCollapsible) { _statusFlags |= StatusFlags.KeepState; } int finiteFormatWidth = settings.GetFiniteFormatWidth(paragraphWidth); FullTextState fullText = FullTextState.Create(settings, cpFirst, finiteFormatWidth); // formatting the line FormatLine( fullText, cpFirst, lineLength, fullText.FormatWidth, finiteFormatWidth, paragraphWidth, lineFlags, null // collapsingSymbol ); } ////// Finalizing full text line /// ~FullTextLine() { DisposeInternal(true); } ////// Releasing the line's unmanaged resource /// public override void Dispose() { DisposeInternal(false); GC.SuppressFinalize(this); } ////// Disposing LS unmanaged memory for text line /// ////// Critical - as this calls LoFinalizeLine, LoDestroyLine and _ploline.Value which /// are all critical functions. /// Safe - as this does not take any pointer parameters that it passes directly to /// the Critical functions. _ploline.Value is critical for set. /// [SecurityCritical, SecurityTreatAsSafe] private void DisposeInternal(bool finalizing) { if (_ploline.Value != System.IntPtr.Zero) { UnsafeNativeMethods.LoDisposeLine(_ploline.Value, finalizing); _ploline.Value = System.IntPtr.Zero; GC.KeepAlive(this); } } ////// Empty private constructor /// ////// Critical - as this calls the constructor for SecurityCriticalDataForSet. /// Safe - as this just initializes it with the default value. /// [SecurityCritical, SecurityTreatAsSafe] private FullTextLine() { _metrics = new TextMetrics(); _ploline = new SecurityCriticalDataForSet(IntPtr.Zero); } /// /// format text line using LS /// /// state of the full text backing store /// first cp to format /// character length of the line /// width used to format /// width used to detect overflowing of format result /// paragraph width /// line formatting control flags /// line end collapsing symbol ////// Critical - as this calls the setter for _ploline.Value which is type SecurityCriticalDataForSet. /// _ploc is critical for set as it's required to properly match the LS context used to /// format the line during display time. /// Safe - as this doesn't get set to a random parameter passed in but rather to a value returned /// by a safe function TextFormatterContext.CreateLine(). /// [SecurityCritical, SecurityTreatAsSafe] private void FormatLine( FullTextState fullText, int cpFirst, int lineLength, int formatWidth, int finiteFormatWidth, int paragraphWidth, LineFlags lineFlags, FormattedTextSymbols collapsingSymbol ) { _metrics._formatter = fullText.Formatter; Debug.Assert(_metrics._formatter != null); TextStore store = fullText.TextStore; TextStore markerStore = fullText.TextMarkerStore; FormatSettings settings = store.Settings; ParaProp pap = settings.Pap; _paragraphTextDecorations = pap.TextDecorations; if (_paragraphTextDecorations != null) { if (_paragraphTextDecorations.Count != 0) { _defaultTextDecorationsBrush = pap.DefaultTextDecorationsBrush; } else { _paragraphTextDecorations = null; } } // acquiring LS context TextFormatterContext context = _metrics._formatter.AcquireContext(fullText, IntPtr.Zero); LsLInfo plslineInfo = new LsLInfo(); LsLineWidths lineWidths = new LsLineWidths(); fullText.SetTabs(context); int lscpLineLength = 0; // line length in LSCP if (lineLength > 0) { // line length is previously known (e.g. during optimal paragraph formatting), // prefetch lsruns up to the specified line length. lscpLineLength = PrefetchLSRuns(store, cpFirst, lineLength); } IntPtr ploline; LsErr lserr = context.CreateLine( cpFirst, lscpLineLength, formatWidth, lineFlags, IntPtr.Zero, // single-line formatting does not require break record out ploline, out plslineInfo, out _depthQueryMax, out lineWidths ); // Did we exceed the LineServices maximum line width? if (lserr == LsErr.TooLongParagraph) { // Determine where to insert a fake line break. FullTextState.CpMeasured // is a reasonable estimate since we know the nominal widths up to that // point fit within the margin. int cpLimit = fullText.CpMeasured; int subtract = 1; for (;;) { // The line must contain at least one character position. if (cpLimit < 1) { cpLimit = 1; } store.InsertFakeLineBreak(cpLimit); lserr = context.CreateLine( cpFirst, lscpLineLength, formatWidth, lineFlags, IntPtr.Zero, // single-line formatting does not require break record out ploline, out plslineInfo, out _depthQueryMax, out lineWidths ); if (lserr != LsErr.TooLongParagraph || cpLimit == 1) { // We're done or can't chop off any more text. break; } else { // Chop off more text and try again. Double the amount of // text we chop off each time so we retry too many times. cpLimit = fullText.CpMeasured - subtract; subtract *= 2; } } } _ploline.Value = ploline; // get the exception in context before it is released Exception callbackException = context.CallbackException; // release the context context.Release(); if(lserr != LsErr.None) { GC.SuppressFinalize(this); if(callbackException != null) { // rethrow exception thrown in callbacks throw WrapException(callbackException); } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.CreateLineFailure, lserr), lserr); } } // keep context alive at least till here GC.KeepAlive(context); unsafe { // construct text metrics for the line _metrics.Compute( fullText, cpFirst, paragraphWidth, collapsingSymbol, ref lineWidths, &plslineInfo ); } // keep record for min width as we may be formatting min/max _textMinWidthAtTrailing = lineWidths.upMinStartTrailing - _metrics._textStart; if (collapsingSymbol != null) { _collapsingSymbol = collapsingSymbol; _textMinWidthAtTrailing += _metrics._formatter.RealToIdeal(collapsingSymbol.Width); } else { // overflow detection for potential collapsible line if (_metrics._textStart + _metrics._textWidthAtTrailing > finiteFormatWidth) { // line has overflowed _statusFlags |= StatusFlags.HasOverflowed; // let's keep the full text state around. We'll need it later for collapsing _fullText = fullText; } } if ( fullText != null && ( fullText.KeepState || (_statusFlags & StatusFlags.KeepState) != 0 ) ) { // the state of full text is to be kept after formatting is done _fullText = fullText; } // retain all line properties for interactive operations _ploc = context.Ploc; _cpFirst = cpFirst; _paragraphWidth = paragraphWidth; if (pap.RightToLeft) _statusFlags |= StatusFlags.RightToLeft; if (plslineInfo.fForcedBreak != 0) _statusFlags |= StatusFlags.IsTruncated; // retain the state of plsruns _plsrunVector = store.PlsrunVector; _lsrunsMainText = store.LsrunList; if (markerStore != null) _lsrunsMarkerText = markerStore.LsrunList; // we store the text source in the line in case drawing code calls // the TextSource to find out the text effect index. // _textSource = settings.TextSource; } ////// Wraps a caught exception in a new exception object of the same type, if possible. /// Otherwise just return the original exception. /// private static Exception WrapException(Exception caughtException) { // We're going to try to create a new exception of the same type as caughtException. Type t = caughtException.GetType(); // Make sure the type is public to avoid MethodAccessException in partial trust. if (t.IsPublic) { // Look for a public instance constructor with signature ctor(Exception) ConstructorInfo constructor = t.GetConstructor( new Type[] { typeof(Exception) } ); if (constructor != null) { return (Exception)constructor.Invoke( new object[] { caughtException } ); } // Look for a public instance constructor with signature ctor(string,Exception) constructor = t.GetConstructor( new Type[] { typeof(string), typeof(Exception) } ); if (constructor != null) { return (Exception)constructor.Invoke( new object[] { caughtException.Message, caughtException } ); } } // We couldn't find an appropriate constructor so fall back to returning the original // exception object. We don't want to wrap the exception in some arbitrary exception type // because the client may have thrown the exception and may want to catch it higher up // the stack. // // The disadvantage of throwing the same object again is the original stack is lost. This // makes debugging harder (partially mitigated by the stack trace string available via // the Data property -- but only in full dumps), and means Watson errors will be bucketized // only based on the current stack, i.e., FormatLine. Hopefully this case will be rare. // return caughtException; } ////// Append line end collapsing symbol /// private void AppendCollapsingSymbol( FormattedTextSymbols symbol ) { Debug.Assert(_collapsingSymbol == null && symbol != null); _collapsingSymbol = symbol; int symbolIdealWidth = _metrics._formatter.RealToIdeal(symbol.Width); _metrics.AppendCollapsingSymbolWidth(symbolIdealWidth); _textMinWidthAtTrailing += symbolIdealWidth; } ////// Prefetch the lsruns up to the point of the specified line length and map /// the specified length to the corresponding LSCP length. /// ////// See comment in the remark section of FullTextState.GetBreakpointInternalCp. /// private int PrefetchLSRuns( TextStore store, int cpFirst, int lineLength ) { Debug.Assert(lineLength > 0); LSRun lsrun; int prefetchLength = 0; int lscpLineLength = 0; int lastSpanLength = 0; int lastRunLength = 0; do { Plsrun plsrun; int lsrunOffset; int lsrunLength; lsrun = store.FetchLSRun( cpFirst + lscpLineLength, out plsrun, out lsrunOffset, out lsrunLength ); if (lineLength == prefetchLength && lsrun.Type == Plsrun.Reverse) { break; } lastSpanLength = lsrunLength; lastRunLength = lsrun.Length; lscpLineLength += lastSpanLength; prefetchLength += lastRunLength; } while ( !TextStore.IsNewline(lsrun.Type) && lineLength >= prefetchLength ); // calibrate the LSCP length to the LSCP equivalence of the last CP of the line if (prefetchLength == lineLength || lastSpanLength == lastRunLength) return lscpLineLength - prefetchLength + lineLength; Invariant.Assert(prefetchLength - lineLength == lastRunLength); return lscpLineLength - lastSpanLength; } ////// Draw line /// /// drawing context /// drawing origin /// indicate the inversion of the drawing surface public override void Draw( DrawingContext drawingContext, Point origin, InvertAxes inversion ) { if (drawingContext == null) { throw new ArgumentNullException("drawingContext"); } if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } MatrixTransform antiInversion = TextFormatterImp.CreateAntiInversionTransform( inversion, _metrics._formatter.IdealToReal(_paragraphWidth), _metrics._formatter.IdealToReal(_metrics._height) ); if (antiInversion == null) { DrawTextLine(drawingContext, origin, null); } else { // Apply anti-inversion transform to correct the visual drawingContext.PushTransform(antiInversion); try { DrawTextLine(drawingContext, origin, antiInversion); } finally { drawingContext.Pop(); } } } ////// Draw complex text line /// /// drawing surface /// offset to the line origin /// anti-inversion transform applied on the surface ////// Critical - as this calls Critical function LoDisplayLine. /// Safe - as this doesn't take any pointer parameters that are passed to /// LoDisplayLine directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void DrawTextLine( DrawingContext drawingContext, Point origin, MatrixTransform antiInversion ) { Rect boundingBox = Rect.Empty; if (_ploline.Value != System.IntPtr.Zero) { TextFormatterContext context; LsErr lserr = LsErr.None; LSRECT rect = new LSRECT(0, 0, _metrics._textWidthAtTrailing, _metrics._height); // DrawingState needs to be properly disposed after performing actual drawing operations. using (DrawingState drawingState = new DrawingState(drawingContext, origin, antiInversion, this)) { context = _metrics._formatter.AcquireContext( drawingState, _ploc.Value ); // set the collector and send the line to LS to draw context.EmptyBoundingBox(); // LS line reference origin LSPOINT lsRefOrigin = new LSPOINT(0, _metrics._baselineOffset); lserr = UnsafeNativeMethods.LoDisplayLine( _ploline.Value, ref lsRefOrigin, 1, // 0 - opaque, 1 - transparent ref rect ); } boundingBox = context.BoundingBox; // get the exception in context before it is released Exception callbackException = context.CallbackException; context.Release(); if(lserr != LsErr.None) { if(callbackException != null) { // rethrow exception thrown in callbacks throw callbackException; } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.CreateLineFailure, lserr), lserr); } } // keep context alive at least til here GC.KeepAlive(context); } if (_collapsingSymbol != null) { // draw collapsing symbol if any Point vectorToOrigin = new Point(); if (antiInversion != null) { vectorToOrigin = origin; origin.X = origin.Y = 0; } boundingBox.Union(DrawCollapsingSymbol(drawingContext, origin, vectorToOrigin)); } BuildOverhang(origin, boundingBox); _statusFlags |= StatusFlags.BoundingBoxComputed; } ////// Draw line end collapsing symbol /// private Rect DrawCollapsingSymbol( DrawingContext drawingContext, Point lineOrigin, Point vectorToLineOrigin ) { int symbolIdealWidth = _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); Point symbolOrigin = LSRun.UVToXY( lineOrigin, vectorToLineOrigin, LSLineUToParagraphU(_metrics._textStart + _metrics._textWidthAtTrailing - symbolIdealWidth), _metrics._baselineOffset, _metrics._formatter.ToReal, this ); return _collapsingSymbol.Draw(drawingContext, symbolOrigin); } ////// Make sure the bounding box is calculated /// private void CheckBoundingBox() { if ((_statusFlags & StatusFlags.BoundingBoxComputed) == 0) { DrawTextLine(null, new Point(0, 0), null); } Debug.Assert((_statusFlags & StatusFlags.BoundingBoxComputed) != 0); } ////// Client to collapse the line to fit for display /// /// a list of collapsing properties public override TextLine Collapse( params TextCollapsingProperties[] collapsingPropertiesList ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if ( !HasOverflowed && (_statusFlags & StatusFlags.KeepState) == 0) { // Attempt to collapse a non-overflowed line results in the original line returned return this; } if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) throw new ArgumentNullException("collapsingPropertiesList"); TextCollapsingProperties collapsingProp = collapsingPropertiesList[0]; double constraintWidth = collapsingProp.Width; if (constraintWidth >= Width) { // constraining width is greater than original line width, no collapsing neeeded. return this; } FormattedTextSymbols symbol = null; if (collapsingProp.Symbol != null) { // create formatted collapsing symbol symbol = new FormattedTextSymbols( _metrics._formatter.GlyphingCache, collapsingProp.Symbol, RightToLeft, _metrics._formatter.ToIdeal ); constraintWidth -= symbol.Width; } Debug.Assert(_fullText != null); FullTextLine line = new TextMetrics.FullTextLine(); // collapsing preserves original line metrics Debug.Assert(_metrics._height > 0); line._metrics._formatter = _metrics._formatter; line._metrics._height = _metrics._height; line._metrics._baselineOffset = _metrics._baselineOffset; if (constraintWidth > 0) { // format main text line with constraint width int finiteFormatWidth = _fullText.TextStore.Settings.GetFiniteFormatWidth( _metrics._formatter.RealToIdeal(constraintWidth) ); bool forceWrap = _fullText.ForceWrap; _fullText.ForceWrap = true; if ((_statusFlags & StatusFlags.KeepState) != 0) { // inherit this flag so the collapsed line retains full text state too. line._statusFlags |= StatusFlags.KeepState; } line.FormatLine( _fullText, _cpFirst, 0, // no line length limit finiteFormatWidth, finiteFormatWidth, _paragraphWidth, // collapsed line is still bound to the original paragraph width (collapsingProp.Style == TextCollapsingStyle.TrailingCharacter ? LineFlags.BreakAlways : LineFlags.None), symbol ); _fullText.ForceWrap = forceWrap; line._metrics._cchDepend = 0; // no dependency } else if (symbol != null) { line.AppendCollapsingSymbol(symbol); } if (line._metrics._cchLength < Length) { line._collapsedRange = new TextCollapsedRange( _cpFirst + line._metrics._cchLength, Length - line._metrics._cchLength, Width - line.Width ); // collapsed line has the original length line._metrics._cchLength = Length; } // mark the indication flags signify collapsing line._statusFlags |= StatusFlags.HasCollapsed; line._statusFlags &= ~StatusFlags.HasOverflowed; return line; } ////// Client to get a collection of collapsed cha----r ranges after a line has been collapsed /// public override IListGetTextCollapsedRanges() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if (_collapsedRange == null) return null; Debug.Assert(HasCollapsed); return new TextCollapsedRange[] { _collapsedRange }; } /// /// Client to get the character hit corresponding to the specified /// distance from the beginning of the line. /// /// distance in text flow direction from the beginning of the line ///character hit public override CharacterHit GetCharacterHitFromDistance( double distance ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } return CharacterHitFromDistance(ParagraphUToLSLineU(_metrics._formatter.RealToIdeal(distance))); } ////// Get character hit from specified hittest distance relative to line start /// private CharacterHit CharacterHitFromDistance(int hitTestDistance) { // assuming the first cp of the line CharacterHit characterHit = new CharacterHit(_cpFirst, 0); if(_ploline.Value == IntPtr.Zero) { // Returning the first cp for the empty line return characterHit; } if ( HasCollapsed && _collapsedRange != null && _collapsingSymbol != null ) { int lineEndDistance = _metrics._textStart + _metrics._textWidthAtTrailing; int rangeWidth = _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); if (hitTestDistance >= lineEndDistance - rangeWidth) { if (lineEndDistance - hitTestDistance < rangeWidth / 2) { // The hit-test distance is within the trailing edge of the collapsed range, // return the character hit at the beginning of the range. return new CharacterHit(_collapsedRange.TextSourceCharacterIndex, _collapsedRange.Length); } // The hit-test distance is within the leading edge of the collapsed range, // return the character hit at the beginning of the range. return new CharacterHit(_collapsedRange.TextSourceCharacterIndex, 0); } } LsTextCell lsTextCell; LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; int actualSublineCount; QueryLinePointPcp( new Point(hitTestDistance, 0), sublineInfo, out actualSublineCount, out lsTextCell ); if (actualSublineCount > 0 && lsTextCell.dupCell > 0) { // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); // Assuming caret stops at every codepoint. // // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. int caretStopCount = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; int codepointsToNextCaretStop = lsrun.IsHitTestable ? 1 : lsrun.Length; if ( lsrun.IsHitTestable && ( lsrun.HasExtendedCharacter || lsrun.NeedsCaretInfo) ) { // A hit-testable run with caret stops at every cluster boundaries, // e.g. run with combining mark, with extended characters or complex scripts such as Thai codepointsToNextCaretStop = caretStopCount; caretStopCount = 1; } // All the UV coordinate in subline are in main direction. If the last subline where // we hittest runs in the opposite direction, the logical advance from text cell start cp // will be negative value. int direction = (sublineInfo[actualSublineCount - 1].lstflowSubLine == sublineInfo[0].lstflowSubLine) ? 1 : -1; hitTestDistance = (hitTestDistance - lsTextCell.pointUvStartCell.x) * direction; Invariant.Assert(caretStopCount > 0); int wholeAdvance = lsTextCell.dupCell / caretStopCount; int remainingAdvance = lsTextCell.dupCell % caretStopCount; for (int i = 0; i < caretStopCount; i++) { int caretAdvance = wholeAdvance; if (remainingAdvance > 0) { caretAdvance++; remainingAdvance--; } if (hitTestDistance <= caretAdvance) { if (hitTestDistance > caretAdvance / 2) { // hittest at the trailing edge of the current caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + i, codepointsToNextCaretStop); } // hittest at the leading edge of the current caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + i, 0); } hitTestDistance -= caretAdvance; } // hittest beyond the last caret stop, return the trailing edge of the last caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + caretStopCount - 1, codepointsToNextCaretStop); } return characterHit; } ////// Client to get the distance from the beginning of the line from the specified character hit. /// /// index to text source's character store ///distance in text flow direction from the beginning of the line. public override double GetDistanceFromCharacterHit( CharacterHit characterHit ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); return _metrics._formatter.IdealToReal(LSLineUToParagraphU(DistanceFromCharacterHit(characterHit))); } ////// Get hittest distance relative to line start from specified character hit /// private int DistanceFromCharacterHit(CharacterHit characterHit) { int hitTestDistance = 0; if (_ploline.Value == IntPtr.Zero) { // Returning start of the line for empty line return hitTestDistance; } if (characterHit.FirstCharacterIndex >= _cpFirst + _metrics._cchLength) { // Returning line width for character hit beyond the last caret stop return _metrics._textStart + _metrics._textWidthAtTrailing; } if ( HasCollapsed && _collapsedRange != null && characterHit.FirstCharacterIndex >= _collapsedRange.TextSourceCharacterIndex ) { // The current character hit is beyond the beginning of the collapsed range int lineEndDistance = _metrics._textStart + _metrics._textWidthAtTrailing; if ( characterHit.FirstCharacterIndex >= _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length || characterHit.TrailingLength != 0 || _collapsingSymbol == null ) { // The current character hit either hits outside, // or it's at the trailing edge of the collapsed range return lineEndDistance; } return lineEndDistance - _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); } int actualSublineCount; LsTextCell lsTextCell; LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; int lscpCurrent = GetInternalCp(characterHit.FirstCharacterIndex); QueryLineCpPpoint( lscpCurrent, sublineInfo, out actualSublineCount, out lsTextCell ); if (actualSublineCount > 0) { return lsTextCell.pointUvStartCell.x + GetDistanceInsideTextCell( lscpCurrent, characterHit.TrailingLength != 0, sublineInfo, actualSublineCount, ref lsTextCell ); } return hitTestDistance; } ////// Get distance from the start of text cell to the specified lscp /// private int GetDistanceInsideTextCell( int lscpCurrent, bool isTrailing, LsQSubInfo[] sublineInfo, int actualSublineCount, ref LsTextCell lsTextCell ) { int distanceInCell = 0; // All the UV coordinate in subline are in main direction. If the last subline where // we hittest runs in the opposite direction, the logical advance from text cell start cp // will be negative value. int direction = (sublineInfo[actualSublineCount - 1].lstflowSubLine == sublineInfo[0].lstflowSubLine) ? 1 : -1; // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. // // Assuming caret stops at every codepoint in the run. int caretStopCount = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; int codepointsFromStartCell = lscpCurrent - lsTextCell.lscpStartCell; // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); if ( lsrun.IsHitTestable && ( lsrun.HasExtendedCharacter || lsrun.NeedsCaretInfo) ) { // A hit-testable run with caret stops at every cluster boundaries, // e.g. run with combining mark, with extended characters or complex scripts such as Thai caretStopCount = 1; } Invariant.Assert(caretStopCount > 0); int wholeAdvance = lsTextCell.dupCell / caretStopCount; int remainingAdvance = lsTextCell.dupCell % caretStopCount; for (int i = 1; i <= caretStopCount; i++) { int caretAdvance = wholeAdvance; if (remainingAdvance > 0) { caretAdvance++; remainingAdvance--; } if (codepointsFromStartCell < i) { if (isTrailing) { // hit-test at the trailing edge of the current caret stop, include the current caret advance return (distanceInCell + caretAdvance) * direction; } // hit-test at the leading edge of the current caret stop, return the accumulated distance return distanceInCell * direction; } distanceInCell += caretAdvance; } // hit-test beyond the last caret stop, return the total accumated distance up to the trailing edge of the last caret stop. return distanceInCell * direction; } ////// Client to get the next character hit for caret navigation /// /// the current character hit ///the next character hit public override CharacterHit GetNextCaretCharacterHit( CharacterHit characterHit ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); if (_ploline.Value == System.IntPtr.Zero) { return characterHit; } int caretStopIndex; int offsetToNextCaretStopIndex; bool found = GetNextOrPreviousCaretStop( characterHit.FirstCharacterIndex, CaretDirection.Forward, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is beyond the last caret stop. return characterHit; } if (caretStopIndex <= characterHit.FirstCharacterIndex && characterHit.TrailingLength != 0) { // We treat trailing length of the current character hit as a flag on the way in. // A non-zero value indicates that it is on the trailing edge of the current // caret stop. At this point, the current caret stop fully encloses the input index, // and the input is at the trailing edge. In this case, we move it to the trailing // edge of the next caret stop. found = GetNextOrPreviousCaretStop( caretStopIndex + offsetToNextCaretStopIndex, CaretDirection.Forward, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // This current index is beyond the last caret stop return characterHit; } return new CharacterHit(caretStopIndex, offsetToNextCaretStopIndex); } // If the current character hit is at the leading edge, // move it to trailing edge of the current caret stop. return new CharacterHit(caretStopIndex, offsetToNextCaretStopIndex); } ////// Client to get the previous character hit for caret navigation /// /// the current character hit ///the previous character hit public override CharacterHit GetPreviousCaretCharacterHit( CharacterHit characterHit ) { return GetPreviousCaretCharacterHitByBehavior(characterHit, CaretDirection.Backward); } ////// Client to get the previous character hit after backspacing /// /// the current character hit ///the character hit after backspacing public override CharacterHit GetBackspaceCaretCharacterHit( CharacterHit characterHit ) { return GetPreviousCaretCharacterHitByBehavior(characterHit, CaretDirection.Backspace); } ////// Calculate previous caret character hit based on the caret action /// private CharacterHit GetPreviousCaretCharacterHitByBehavior( CharacterHit characterHit, CaretDirection direction ) { Debug.Assert(direction == CaretDirection.Backward || direction == CaretDirection.Backspace); if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); if (_ploline.Value == IntPtr.Zero) { return characterHit; } if ( characterHit.FirstCharacterIndex == _cpFirst && characterHit.TrailingLength == 0) { // We are already at the beginning of the line return characterHit; } int caretStopIndex; int offsetToNextCaretStopIndex; bool found = GetNextOrPreviousCaretStop( characterHit.FirstCharacterIndex, direction, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is before the 1st caret stop. return characterHit; } if ( offsetToNextCaretStopIndex != 0 && characterHit.TrailingLength == 0 && caretStopIndex != _cpFirst && caretStopIndex >= characterHit.FirstCharacterIndex ) { // If the current character hit is at the leading edge and it is not at the first caret stop, // move it to leading edge of the previous caret stop. At this point, the current character stop // fully encloses the input index and the input is at the leading edge. found = GetNextOrPreviousCaretStop( caretStopIndex - 1, // position at the character immediately preceding the current caret stop direction, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is before the 1st caret stop. return characterHit; } } // The current chracter hit is either beyond the last caret stop, // or it's at the trailing edge of the current caret stop, // or the current index is at the leading edge of the first caret stop. // // In such cases, move to the leading edge of the closest caret stop. return new CharacterHit(caretStopIndex, 0); } ////// Given a specified current character index, calculate the character index /// to the closest caret stop before or at the current index; and the number /// of codepoints from the closest caret stop to the next caret stop. /// private bool GetNextOrPreviousCaretStop( int currentIndex, CaretDirection direction, out int caretStopIndex, out int offsetToNextCaretStopIndex ) { caretStopIndex = currentIndex; offsetToNextCaretStopIndex = 0; if ( HasCollapsed && _collapsedRange != null && currentIndex >= _collapsedRange.TextSourceCharacterIndex ) { // current index is within collapsed range, caretStopIndex = _collapsedRange.TextSourceCharacterIndex; if (currentIndex < _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length) offsetToNextCaretStopIndex = _collapsedRange.Length; return true; } LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; LsTextCell lsTextCell = new LsTextCell(); int lscpVisisble = GetInternalCp(currentIndex); bool found = FindNextOrPreviousVisibleCp(lscpVisisble, direction, out lscpVisisble); if (!found) { return false; // there is no caret stop anymore in the given direction. } int actualSublineCount; QueryLineCpPpoint( lscpVisisble, sublineInfo, out actualSublineCount, out lsTextCell ); // Locate the current caret stop caretStopIndex = GetExternalCp(lsTextCell.lscpStartCell); if ( actualSublineCount > 0 && lscpVisisble >= lsTextCell.lscpStartCell && lscpVisisble <= lsTextCell.lscpEndCell ) { // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); if (lsrun.IsHitTestable) { if ( lsrun.HasExtendedCharacter || (direction != CaretDirection.Backspace && lsrun.NeedsCaretInfo) ) { // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. offsetToNextCaretStopIndex = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; } else { // caret stops before every codepoint caretStopIndex = GetExternalCp(lscpVisisble); offsetToNextCaretStopIndex = 1; } } else { // run is not hit-testable, caret navigation is not allowed in the run, // the next caret stop is therefore either at the end of the run or the end of the line whichever reached first. offsetToNextCaretStopIndex = Math.Min(Length, lsrun.Length - caretStopIndex + lsrun.OffsetToFirstCp + _cpFirst); } } return true; } ////// Search from the given lscp (inclusive) towards the specified direction for the /// closest navigable cp. Return true is one such cp is found, false otherwise. /// private bool FindNextOrPreviousVisibleCp( int lscp, CaretDirection direction, out int lscpVisisble ) { lscpVisisble = lscp; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); if (direction == CaretDirection.Forward) { while (lscpVisisble < _metrics._lscpLim) { plsrunSpanRider.At(lscpVisisble - _cpFirst); LSRun run = GetRun((Plsrun) plsrunSpanRider.CurrentElement); // When scanning forward, only trailine edges of visiable content are navigable. if (run.IsVisible) { return true; } lscpVisisble += plsrunSpanRider.Length; // move to start of next span } } else { Debug.Assert(direction == CaretDirection.Backward || direction == CaretDirection.Backspace); // lscpCurrent can be right after the end of the line, we snap it back to be at the end of the line. lscpVisisble = Math.Min(lscpVisisble, _metrics._lscpLim - 1); while (lscpVisisble >= _cpFirst) { plsrunSpanRider.At(lscpVisisble - _cpFirst); LSRun run = GetRun((Plsrun) plsrunSpanRider.CurrentElement); // When scanning backward, visiable content has caret stop at its leading edge. if (run.IsVisible) { return true; } // When scanning backward, the newline sequence has caret stop at its leading edge. if (run.IsNewline) { // set navigable cp at the start of newline sequence. lscpVisisble = _cpFirst + plsrunSpanRider.CurrentSpanStart; return true; } lscpVisisble = _cpFirst + plsrunSpanRider.CurrentSpanStart - 1; // move to the end of previous span } } lscpVisisble = lscp; return false; } ////// Create zerowidth bounds with line height /// private TextBounds[] CreateDegenerateBounds() { return new TextBounds[] { new TextBounds( new Rect(0, 0, 0, Height), (RightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight), null // runBounds ) }; } ////// Create bounds of collapsing symbol /// private TextBounds CreateCollapsingSymbolBounds() { Debug.Assert(_collapsingSymbol != null); return new TextBounds( new Rect(Start + Width - _collapsingSymbol.Width, 0, _collapsingSymbol.Width, Height), (RightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight), null ); } ////// Client to get an array of bounding rectangles of a range of characters within a text line. /// /// index of first character of specified range /// number of characters of the specified range ///an array of bounding rectangles. public override IListGetTextBounds( int firstTextSourceCharacterIndex, int textLength ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if(textLength == 0) { throw new ArgumentOutOfRangeException("textLength", SR.Get(SRID.ParameterMustBeGreaterThanZero)); } if(textLength < 0) { firstTextSourceCharacterIndex += textLength; textLength = -textLength; } if(firstTextSourceCharacterIndex < _cpFirst) { textLength += (firstTextSourceCharacterIndex - _cpFirst); firstTextSourceCharacterIndex = _cpFirst; } if(firstTextSourceCharacterIndex > _cpFirst + _metrics._cchLength - textLength) { textLength = (_cpFirst + _metrics._cchLength - firstTextSourceCharacterIndex); } if (_ploline.Value == IntPtr.Zero) { return CreateDegenerateBounds(); } Point position = new Point(0,0); // get first cp sublines & text cell int firstDepth; LsTextCell firstTextCell; LsQSubInfo[] firstSublines = new LsQSubInfo[_depthQueryMax]; int lscpFirst = GetInternalCp(firstTextSourceCharacterIndex); QueryLineCpPpoint( lscpFirst, firstSublines, out firstDepth, out firstTextCell ); if(firstDepth <= 0) { // this happens for empty line (line containing only EOP) return CreateDegenerateBounds(); } // get last cp sublines & text cell int lastDepth; LsTextCell lastTextCell; LsQSubInfo[] lastSublines = new LsQSubInfo[_depthQueryMax]; int lscpEnd = GetInternalCp(firstTextSourceCharacterIndex + textLength - 1); QueryLineCpPpoint( lscpEnd, lastSublines, out lastDepth, out lastTextCell ); if(lastDepth <= 0) { // This should never happen but if it does, we still cant throw here. // We must return something even though it's a degenerate bounds or // client hittesting code will just crash. Debug.Assert(false); return CreateDegenerateBounds(); } // check if collapsing symbol is wholely selected bool collapsingSymbolSelected = ( _collapsingSymbol != null && _collapsedRange != null && firstTextSourceCharacterIndex < _collapsedRange.TextSourceCharacterIndex && firstTextSourceCharacterIndex + textLength - _collapsedRange.TextSourceCharacterIndex > _collapsedRange.Length / 2 ); TextBounds[] bounds = null; ArrayList boundsList = null; // By default, if the hittested CP is visible, then we want cpFirst to hit // on the leading edge of the first visible cp, and cpEnd to hit on the trailing edge of the // last visible cp. bool isCpFirstTrailing = false; bool isCpEndTrailing = true; if (lscpFirst > firstTextCell.lscpEndCell) { // when cpFirst is after the last visible cp, then it hits the trailing edge of that cp isCpFirstTrailing = true; } if (lscpEnd < lastTextCell.lscpStartCell) { // when cpEnd is before the first visible cp, then it hits the leading edge of that cp isCpEndTrailing = false; } if (firstDepth == lastDepth && firstSublines[firstDepth - 1].lscpFirstSubLine == lastSublines[lastDepth - 1].lscpFirstSubLine) { // first and last cp are within the same subline int count = collapsingSymbolSelected ? 2 : 1; bounds = new TextBounds[count]; bounds[0] = new TextBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x ), 0 ), new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpEnd, isCpEndTrailing, lastSublines, lastDepth, ref lastTextCell ) + lastTextCell.pointUvStartCell.x ), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(firstSublines[firstDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpFirst, lscpEnd + 1) ); if (count > 1) { bounds[1] = CreateCollapsingSymbolBounds(); } } else { // first and last cp are not in the same subline. boundsList = new ArrayList(2); int lscpCurrent = lscpFirst; // The hittested cp can be outside of the returned sublines when it is a hidden cp. // We should not pass beyond the end of the returned sublines. int lscpEndInSubline = Math.Min( lscpEnd, lastSublines[lastDepth - 1].lscpFirstSubLine + lastSublines[lastDepth - 1].lsdcpSubLine - 1 ); int currentDistance = GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x; int baseLevelDepth; CollectTextBoundsToBaseLevel( boundsList, ref lscpCurrent, ref currentDistance, firstSublines, firstDepth, lscpEndInSubline, out baseLevelDepth ); if (baseLevelDepth < lastDepth) { CollectTextBoundsFromBaseLevel( boundsList, ref lscpCurrent, ref currentDistance, lastSublines, lastDepth, baseLevelDepth ); } // Collect the bounds from the start of the immediate enclosing subline of the last LSCP // to the hittested text cell. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU(currentDistance), 0 ), new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpEnd, isCpEndTrailing, lastSublines, lastDepth, ref lastTextCell ) + lastTextCell.pointUvStartCell.x ), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(lastSublines[lastDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, lscpEnd + 1) ) ); } if (bounds == null) { Debug.Assert(boundsList != null); if (boundsList.Count > 0) { if (collapsingSymbolSelected) { // add one more for collapsed symbol AddValidTextBounds(boundsList, CreateCollapsingSymbolBounds()); } bounds = new TextBounds[boundsList.Count]; for (int i = 0; i < boundsList.Count; i++) { bounds[i] = (TextBounds)boundsList[i]; } } else { // No non-zerowidth bounds detected, fallback to the position of first cp // This can happen if hidden run is hittest'd. int u = LSLineUToParagraphU( GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x ); bounds = new TextBounds[] { new TextBounds( LSRun.RectUV( position, new LSPOINT(u, 0), new LSPOINT(u, _metrics._height), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(firstSublines[firstDepth - 1].lstflowSubLine), null ) }; } } return bounds; } /// /// Base level is the highest subline's level that both sets of sublines have in common. /// This method starts collecting text bounds at the specified LSCP. The first bounds being /// collected is the one from the LSCP to the end of its immediate enclosing subline. The /// subsequent bounds are from the end of run to the end of subline of the lower level until /// it reaches the base level. /// private void CollectTextBoundsToBaseLevel( ArrayList boundsList, ref int lscpCurrent, ref int currentDistance, LsQSubInfo[] sublines, int sublineDepth, int lscpEnd, out int baseLevelDepth ) { baseLevelDepth = sublineDepth; if (lscpEnd < sublines[sublineDepth - 1].lscpFirstSubLine + sublines[sublineDepth - 1].lsdcpSubLine) { // The immedidate enclosing subline already contains the lscp end. It means we are already // at base level. return; } // Collect text bounds from the current lscp to the end of the immediate enclosing subline. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(currentDistance), 0), new LSPOINT( LSLineUToParagraphU(GetEndOfSublineDistance(sublines, sublineDepth - 1)), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[sublineDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, sublines[sublineDepth - 1].lscpFirstSubLine + sublines[sublineDepth - 1].lsdcpSubLine) ) ); // Collect text bounds from end of run to the end of subline at lower levels until we reach the // common level. We reach common level when the subline at that level contains the lscpEnd. for ( baseLevelDepth = sublineDepth - 1; baseLevelDepth > 0 && (lscpEnd >= sublines[baseLevelDepth - 1].lscpFirstSubLine + sublines[baseLevelDepth - 1].lsdcpSubLine); baseLevelDepth-- ) { int sublineIndex = baseLevelDepth - 1; AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT( LSLineUToParagraphU(GetEndOfRunDistance(sublines, sublineIndex)), 0 ), new LSPOINT( LSLineUToParagraphU(GetEndOfSublineDistance(sublines, sublineIndex)), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[sublineIndex].lstflowSubLine), CalculateTextRunBounds( sublines[sublineIndex].lscpFirstRun + sublines[sublineIndex].lsdcpRun, sublines[sublineIndex].lscpFirstSubLine + sublines[sublineIndex].lsdcpSubLine ) ) ); } // base level depth must be at least 1 because both cp at least share the main line. Invariant.Assert(baseLevelDepth >= 1); // Move the current LSCP and distance to the end of run on the base level subline. lscpCurrent = sublines[baseLevelDepth - 1].lscpFirstRun + sublines[baseLevelDepth - 1].lsdcpRun; currentDistance = GetEndOfRunDistance(sublines, baseLevelDepth - 1); } ////// Base level is the highest subline's level that both sets of sublines have in common. /// This method starts collecting text bounds at the specified LSCP. The first bounds being collected /// is the one from the LSCP to the start of run at the base level subline. The subsequent bounds are /// from the start of the higher level subline to the start of the run within the same subline, until /// it reaches the immediate enclosing subline of the last LSCP. /// private void CollectTextBoundsFromBaseLevel( ArrayList boundsList, ref int lscpCurrent, ref int currentDistance, LsQSubInfo[] sublines, int sublineDepth, int baseLevelDepth ) { // lscpCurrent is after the run end of the 1st cp. It must not be in the run of the last cp // because the two runs don't overlap at above base level. Invariant.Assert(lscpCurrent <= sublines[baseLevelDepth - 1].lscpFirstRun); // Collect the text bounds from the LSCP to the start of run at the base level subline. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(currentDistance), 0), new LSPOINT( LSLineUToParagraphU(sublines[baseLevelDepth - 1].pointUvStartRun.x), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[baseLevelDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, sublines[baseLevelDepth - 1].lscpFirstRun) ) ); // Collect text bounds from start of subline to start of run at higher level sublines until it // reaches the immediate enclosing subline of the last LSCP. for (int i = baseLevelDepth; i < sublineDepth - 1; i++) { AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(sublines[i].pointUvStartSubLine.x), 0), new LSPOINT( LSLineUToParagraphU(sublines[i].pointUvStartRun.x), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[i].lstflowSubLine), CalculateTextRunBounds( sublines[i].lscpFirstSubLine, sublines[i].lscpFirstRun ) ) ); }; // Move the current LSCP and distance to the start of the immediate enclosing subline. lscpCurrent = sublines[sublineDepth - 1].lscpFirstSubLine; currentDistance = sublines[sublineDepth - 1].pointUvStartSubLine.x; } ////// Return the ending edge of the subline relative to its own flow direction /// private int GetEndOfSublineDistance( LsQSubInfo[] sublines, int index ) { return sublines[index].pointUvStartSubLine.x + ( sublines[index].lstflowSubLine == sublines[0].lstflowSubLine ? sublines[index].dupSubLine : -sublines[index].dupSubLine ); } ////// Return the ending edge of the run relative to its own flow direction. /// private int GetEndOfRunDistance( LsQSubInfo[] sublines, int index ) { return sublines[index].pointUvStartRun.x + ( sublines[index].lstflowSubLine == sublines[0].lstflowSubLine ? sublines[index].dupRun : -sublines[index].dupRun ); } ////// Add non-zero geometry bounds to the bounds list /// private void AddValidTextBounds( ArrayList boundsList, TextBounds bounds ) { if (bounds.Rectangle.Width != 0 && bounds.Rectangle.Height != 0) { boundsList.Add(bounds); } } ////// Compute bounds of runs within the specified range of lscp /// private IListCalculateTextRunBounds(int lscpFirst, int lscpEnd) { if (lscpEnd <= lscpFirst) { // It is possible that we'll get a legitimate case when lscpFirst is // actually greater. That's what happen when the client hittest a hidden // run that follows a reverse block. Since it is a hidden run, LS has // to yield the closest non-hidden place which may be the run preceding // the hidden text. (wchao, PS bug #930976) return null; } int lscp = lscpFirst; int cchLeft = lscpEnd - lscpFirst; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); Point position = new Point(0, 0); IList boundsList = new List (2); while(cchLeft > 0) { plsrunSpanRider.At(lscp - _cpFirst); Plsrun plsrun = (Plsrun)plsrunSpanRider.CurrentElement; int cch = Math.Min(plsrunSpanRider.Length, cchLeft); if(TextStore.IsContent(plsrun)) { LSRun lsrun = GetRun(plsrun); if( lsrun.Type == Plsrun.Text || lsrun.Type == Plsrun.InlineObject) { int cp = GetExternalCp(lscp); int cchBounds = cch; if ( HasCollapsed && _collapsedRange != null && cp <= _collapsedRange.TextSourceCharacterIndex && cp + cchBounds >= _collapsedRange.TextSourceCharacterIndex && cp + cchBounds < _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length) { // Limit the run bounds to only non-collapsed text, // we deal with collapsed text separately as it might have different flow direction. cchBounds = _collapsedRange.TextSourceCharacterIndex - cp; } if (cchBounds > 0) { TextRunBounds bounds = new TextRunBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU( DistanceFromCharacterHit(new CharacterHit(cp, 0)) ), _metrics._baselineOffset - lsrun.BaselineOffset + lsrun.BaselineMoveOffset ), new LSPOINT( LSLineUToParagraphU( DistanceFromCharacterHit(new CharacterHit(cp + cchBounds - 1, 1)) ), _metrics._baselineOffset - lsrun.BaselineOffset + lsrun.BaselineMoveOffset + lsrun.Height ), _metrics._formatter.ToReal, this ), cp, cp + cchBounds, lsrun.TextRun ); boundsList.Add(bounds); } } } cchLeft -= cch; lscp += cch; } return boundsList.Count > 0 ? boundsList : null; } /// /// Client to get a collection of TextRun objects within a line /// public override IList> GetTextRunSpans() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if (_plsrunVector == null) { // return empty textspan when the line doesn't contain text runs. return new TextSpan [0]; } IList > lsrunList = new List >(2); TextRun lastTextRun = null; int cchAcc = 0; int cchLeft = _metrics._cchLength; for (int i = 0; i < _plsrunVector.Count && cchLeft > 0; i++) { Span plsrunSpan = _plsrunVector[i]; int cch = CpCount(plsrunSpan); cch = Math.Min(cch, cchLeft); if (cch > 0) { TextRun textRun = ((LSRun)GetRun((Plsrun)plsrunSpan.element)).TextRun; Debug.Assert(textRun != null); if (lastTextRun != null && textRun != lastTextRun) { Debug.Assert(cchAcc > 0); lsrunList.Add(new TextSpan (cchAcc, lastTextRun)); cchAcc = 0; } lastTextRun = textRun; cchAcc += cch; cchLeft -= cch; } } if (lastTextRun != null) { Debug.Assert(cchAcc > 0); lsrunList.Add(new TextSpan (cchAcc, lastTextRun)); } Debug.Assert(cchLeft == 0); return lsrunList; } /// /// Client to get IndexedGlyphRuns enumerable to enumerate each IndexedGlyphRun object /// in the line. Through IndexedGlyphRun client can obtain glyph information of /// a text source character. /// ////// Critical - as this calls the Critical function UnsafeNativeMethods.LoEnumLine. /// Safe - as the IntPtr passed in is of type SecurityCriticalDataForSet which means /// it is tracked to make sure it can't be set to a random value. /// [SecurityCritical, SecurityTreatAsSafe] public override IEnumerableGetIndexedGlyphRuns() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } IEnumerable result = null; if (_ploline.Value != System.IntPtr.Zero) { TextFormatterContext context = _metrics._formatter.AcquireContext( new DrawingState(null, new Point(0, 0), null, this), _ploc.Value ); // // Kick off line enumeration // LsErr lserr = LsErr.None; LSPOINT point = new LSPOINT(0, 0); lserr = UnsafeNativeMethods.LoEnumLine( _ploline.Value, // line false, // reverse enumeration false, // geometry needed ref point // starting point ); // result result = context.IndexedGlyphRuns; // get the exception in context before it is released Exception callbackException = context.CallbackException; // clear the context context.ClearIndexedGlyphRuns(); context.Release(); if (lserr != LsErr.None) { if (callbackException != null) { // rethrow exception thrown in callbacks throw callbackException; } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.EnumLineFailure, lserr), lserr); } } } return result; } /// /// Client to acquire a state at the point where line is broken by line breaking process; /// can be null when the line ends by the ending of the paragraph. Client may pass this /// value back to TextFormatter as an input argument to TextFormatter.FormatLine when /// formatting the next line within the same paragraph. /// ////// Critical - as this calls TextMetrics.GetTextLineBreak. /// Safe - as it does not take parameter that being passed to the critical method or /// return sensitive data from the critical method. /// [SecurityCritical, SecurityTreatAsSafe] public override TextLineBreak GetTextLineBreak() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if ((_statusFlags & StatusFlags.HasCollapsed) != 0) { // collapsed line has no line break state to transfer return null; } return _metrics.GetTextLineBreak(IntPtr.Zero); } ////// Client to get the number of whitespace characters at the end of the line. /// public override int TrailingWhitespaceLength { get { // figure out number of trailing whitespaces if(_metrics._textWidth == _metrics._textWidthAtTrailing) { // LS doesnt see any trailing space (last character in a line or character before // EOP is not space). We count only the length of EOP run that we count as our // trailing whitespace. return _metrics._cchNewline; } else { // LS sees some trailing spaces (last character in a line or character before EOP // is space). We calculate number of trailing spaces based on the cp following // the last non-trailing space recognized by LS. CharacterHit characterHit = CharacterHitFromDistance(_metrics._textWidthAtTrailing + _metrics._textStart); return _cpFirst + _metrics._cchLength - characterHit.FirstCharacterIndex - characterHit.TrailingLength; } } } ////// Client to get the number of text source positions of this line /// public override int Length { get { return _metrics.Length; } } ////// Client to get the number of characters following the last character /// of the line that may trigger reformatting of the current line. /// public override int DependentLength { get { return _metrics.DependentLength; } } ////// Client to get the number of newline characters at line end /// public override int NewlineLength { get { return _metrics.NewlineLength; } } ////// Client to get distance from paragraph start to line start /// public override double Start { get { return _metrics.Start; } } ////// Client to get the total width of this line /// public override double Width { get { return _metrics.Width; } } ////// Client to get the total width of this line including width of whitespace characters at the end of the line. /// public override double WidthIncludingTrailingWhitespace { get { return _metrics.WidthIncludingTrailingWhitespace; } } ////// Client to get the height of the line /// public override double Height { get { return _metrics.Height; } } ////// Client to get the height of the text (or other content) in the line; this property may differ from the Height /// property if the client specified the line height /// public override double TextHeight { get { return _metrics.TextHeight; } } ////// Client to get the distance from top to baseline of this text line /// public override double Baseline { get { return _metrics.Baseline; } } ////// Client to get the distance from the top of the text (or other content) to the baseline of this text line; /// this property may differ from the Baseline property if the client specified the line height /// public override double TextBaseline { get { return _metrics.TextBaseline; } } ////// Client to get the distance from the before edge of line height /// to the baseline of marker of the line if any. /// public override double MarkerBaseline { get { return _metrics.MarkerBaseline; } } ////// Client to get the overall height of the list items marker of the line if any. /// public override double MarkerHeight { get { return _metrics.MarkerHeight; } } ////// Client to get the height of the actual black of the line /// public override double Extent { get { CheckBoundingBox(); return _overhang.Extent; } } ////// Client to get the distance covering all black preceding the leading edge of the line. /// public override double OverhangLeading { get { CheckBoundingBox(); return _overhang.Leading; } } ////// Client to get the distance covering all black following the trailing edge of the line. /// public override double OverhangTrailing { get { CheckBoundingBox(); return _overhang.Trailing; } } ////// Client to get the distance from the after edge of line height to the after edge of the extent of the line. /// public override double OverhangAfter { get { CheckBoundingBox(); return _overhang.Extent - Height - _overhang.Before; } } ////// Client to get a boolean value indicates whether content of the line overflows /// the specified paragraph width. /// public override bool HasOverflowed { get { return (_statusFlags & StatusFlags.HasOverflowed) != 0; } } ////// Client to get a boolean value indicates whether a line has been collapsed /// public override bool HasCollapsed { get { return (_statusFlags & StatusFlags.HasCollapsed) != 0; } } ////// Client to get a Boolean flag indicating whether the line is truncated in the /// middle of a word. This flag is set only when TextParagraphProperties.TextWrapping /// is set to TextWrapping.Wrap and a single word is longer than the formatting /// paragraph width. In such situation, TextFormatter truncates the line in the middle /// of the word to honor the desired behavior specified by TextWrapping.Wrap setting. /// public override bool IsTruncated { get { return (_statusFlags & StatusFlags.IsTruncated) != 0; } } ////// Client to get the index of the first cp of the line. /// public int CpFirst { get { return _cpFirst; } } ////// Text source of the main text for the line /// public TextSource TextSource { get { return _textSource; } } ////// Wrapper to LoQueryLinePointPcp /// ////// Critical - as this calls Critical function LoQueryLinePointPcp. /// Safe - as this doesn't take any parameters that are passed to /// LoQueryLineCpPpoint directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void QueryLinePointPcp( Point ptQuery, LsQSubInfo[] subLineInfo, out int actualDepthQuery, out LsTextCell lsTextCell ) { Debug.Assert(_ploline.Value != IntPtr.Zero); LsErr lserr = LsErr.None; lsTextCell = new LsTextCell(); unsafe { fixed(LsQSubInfo* plsqsubl = subLineInfo) { LSPOINT pt = new LSPOINT((int)ptQuery.X, (int)ptQuery.Y); lserr = UnsafeNativeMethods.LoQueryLinePointPcp( _ploline.Value, ref pt, subLineInfo.Length, (System.IntPtr)plsqsubl, out actualDepthQuery, out lsTextCell ); } } if(lserr != LsErr.None) { TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.QueryLineFailure, lserr), lserr); } if (lsTextCell.lscpEndCell < lsTextCell.lscpStartCell) { // When hit-testing is done on a generated hyphen of a hyphenated word, LS can only tell // the start LSCP and not the end LSCP. Argurably this is LS bug. In such situation they // should assume the end LSCP being the last LSCP of the line. // // However our code assumes that LS must tell both and the text cell must have size greater // than one codepoint. We count on that to reliably advance the caret position. // // The LSPTS bug#1005 has been filed and while we are still debating, we need to unblock // ourselves. What we can do is to assume that the next caret stop in this case is always // the next codepoint. lsTextCell.lscpEndCell = lsTextCell.lscpStartCell; } } ////// Wrapper to LoQueryLineCpPpoint /// ////// Critical - as this calls Critical function LoQueryLineCpPpoint. /// Safe - as this doesn't take any parameters that are passed to /// LoQueryLineCpPpoint directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void QueryLineCpPpoint( int lscpQuery, LsQSubInfo[] subLineInfo, out int actualDepthQuery, out LsTextCell lsTextCell ) { Debug.Assert(_ploline.Value != IntPtr.Zero); LsErr lserr = LsErr.None; lsTextCell = new LsTextCell(); // Never hit LS with any LSCP beyond its last, the result is unreliable and varies between drops. int lscpValidQuery = (lscpQuery < _metrics._lscpLim ? lscpQuery : _metrics._lscpLim - 1); unsafe { fixed(LsQSubInfo* plsqsubl = subLineInfo) { lserr = UnsafeNativeMethods.LoQueryLineCpPpoint( _ploline.Value, lscpValidQuery, subLineInfo.Length, (System.IntPtr)plsqsubl, out actualDepthQuery, out lsTextCell ); } } if(lserr != LsErr.None) { TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.QueryLineFailure, lserr), lserr); } if (lsTextCell.lscpEndCell < lsTextCell.lscpStartCell) { // When hit-testing is done on a generated hyphen of a hyphenated word, LS can only tell // the start LSCP and not the end LSCP. Argurably this is LS bug. In such situation they // should assume the end LSCP being the last LSCP of the line. // // However our code assumes that LS must tell both and the text cell must have size greater // than one codepoint. We count on that to reliably advance the caret position. // // The LSPTS bug#1005 has been filed and while we are still debating, we need to unblock // ourselves. What we can do is to assume that the next caret stop in this case is always // the next codepoint. lsTextCell.lscpEndCell = lsTextCell.lscpStartCell; } } ////// Convert a U distance relative to start of LS line to U distance relative /// to the leading edge of paragraph. /// /// a U distance relative to start of ploline ///another U distance relative to paragraph start internal int LSLineUToParagraphU(int u) { return u + _metrics._paragraphToText - _metrics._textStart; } ////// Convert a U distance relative to the leading edge of paragraph /// to U distance relative to start of LS line. /// /// a U distance relative to paragraph start ///another U distance relative to start of ploline internal int ParagraphUToLSLineU(int u) { return u - _metrics._paragraphToText + _metrics._textStart; } internal int BaselineOffset { get { return _metrics._baselineOffset; } } internal int ParagraphWidth { get { return _paragraphWidth; } } internal double MinWidth { get { return _metrics._formatter.IdealToReal(_textMinWidthAtTrailing + _metrics._textStart); } } internal bool RightToLeft { get { return (_statusFlags & StatusFlags.RightToLeft) != 0; } } internal TextFormatterImp Formatter { get { return _metrics._formatter; } } internal TextDecorationCollection TextDecorations { get { return _paragraphTextDecorations; } } internal Brush DefaultTextDecorationsBrush { get { return _defaultTextDecorationsBrush; } } #if DEBUG internal FullTextState FullTextState { get { return _fullText; } } #endif private void BuildOverhang(Point origin, Rect boundingBox) { if(boundingBox.IsEmpty) { _overhang.Leading = _overhang.Trailing = 0; _overhang.Before = 0; _overhang.Extent = 0; } else { // Move the bounding box to the coordinate relative to the line drawing origin. // The following computation of overhang values need to be done independent to // drawing origin. boundingBox.X -= origin.X; boundingBox.Y -= origin.Y; if (RightToLeft) { double paragraphWidth = _metrics._formatter.IdealToReal(_paragraphWidth); _overhang.Leading = paragraphWidth - Start - boundingBox.Right; _overhang.Trailing = boundingBox.Left - (paragraphWidth - Start - Width); } else { _overhang.Leading = boundingBox.Left - Start; _overhang.Trailing = Start + Width - boundingBox.Right; } _overhang.Extent = boundingBox.Bottom - boundingBox.Top; _overhang.Before = -boundingBox.Top; } } ////// Overhang metrics /// private struct Overhang { internal double Leading; internal double Trailing; internal double Extent; internal double Before; } #region lsrun/cp mapping ////// Map text source CP to internal LSCP /// internal int GetInternalCp(int cp) { int lscp = _cpFirst; int cpTarget = cp; cp = lscp; foreach(Span span in _plsrunVector) { int ccp = CpCount(span); if(ccp > 0) { if(cp + ccp > cpTarget) { lscp += (ccp == span.length ? cpTarget - cp : 0); break; } cp += ccp; } lscp += span.length; } return lscp; } ////// Map internal LSCP to text source cp /// internal int GetExternalCp(int lscp) { if (lscp >= _metrics._lscpLim) { if (_collapsedRange != null) return _collapsedRange.TextSourceCharacterIndex; return _cpFirst + _metrics._cchLength; } int offsetToFirstCp; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); // skip lscp until we find one with valid map do { plsrunSpanRider.At(lscp - _cpFirst); offsetToFirstCp = GetRun(((Plsrun)plsrunSpanRider.CurrentElement)).OffsetToFirstCp; } while(offsetToFirstCp < 0 && ++lscp < _metrics._lscpLim); return offsetToFirstCp + lscp - plsrunSpanRider.CurrentSpanStart; } ////// Count actual cp of an lsrun /// /// span of plsrun ///lsrun actual cp internal int CpCount(Span plsrunSpan) { Plsrun plsrun = (Plsrun)plsrunSpan.element; plsrun = TextStore.ToIndex(plsrun); // Inline object, text or linebreak yields as many cp as what client specifies. // lsrun known only to LS e.g. reverse, yields no actual cp, if(plsrun >= Plsrun.FormatAnchor) { LSRun lsrun = GetRun(plsrun); return lsrun.Length; } return 0; } ////// Get LSRun from plsrun /// internal LSRun GetRun(Plsrun plsrun) { ArrayList lsruns = _lsrunsMainText; if (TextStore.IsMarker(plsrun)) { lsruns = _lsrunsMarkerText; } plsrun = TextStore.ToIndex(plsrun); return (LSRun)( TextStore.IsContent(plsrun) ? lsruns[(int)(plsrun - Plsrun.FormatAnchor)] : TextStore.ControlRuns[(int)plsrun] ); } #endregion } } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved. //------------------------------------------------------------------------ // // Microsoft Windows Client Platform // Copyright (C) Microsoft Corporation, 2001 // // File: FullTextLine.cs // // Contents: Complex implementation of TextLine // // Created: 5-6-2002 Worachai Chaoweeraprasit (wchao) // //----------------------------------------------------------------------- using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Security; using System.Windows; using System.Windows.Media; using System.Windows.Media.TextFormatting; using MS.Internal; using MS.Internal.Shaping; using SR = MS.Internal.PresentationCore.SR; using SRID = MS.Internal.PresentationCore.SRID; namespace MS.Internal.TextFormatting { ////// Make FullTextLine nested type of TextMetrics to allow full access to TextMetrics private members /// internal partial struct TextMetrics : ITextMetrics { ////// Complex implementation of TextLine /// /// TextLine implementation around /// o Line Services /// o OpenType Services Library /// o Implementation of Unicode Bidirectional algorithm /// o Complex script itemizer /// o Composite font with generic glyph hunting algorithm /// internal class FullTextLine : TextLine { private TextMetrics _metrics; // Text metrics private int _cpFirst; // character index to the first charcter of the line private int _depthQueryMax; // maximum depth of reversals used in querying private int _paragraphWidth; // paragraph width private int _textMinWidthAtTrailing; // smallest text width excluding trailing whitespaces private SecurityCriticalDataForSet_ploline; // actual LS line private SecurityCriticalDataForSet _ploc; // actual LS context private Overhang _overhang; // overhang metrics private StatusFlags _statusFlags; // status flags of the line private SpanVector _plsrunVector; // plsrun span vector indexed by lscp private ArrayList _lsrunsMainText; // list of lsrun of main text private ArrayList _lsrunsMarkerText; // list of lsrun of marker text private FullTextState _fullText; // full text state kept for collapsing purpose (only have it when StatusFlags.HasOverflowed is set) private FormattedTextSymbols _collapsingSymbol; // line-end collapsing symbol private TextCollapsedRange _collapsedRange; // line-end collapsed range private TextSource _textSource; // Text Source of the main text for the line private TextDecorationCollection _paragraphTextDecorations; // Paragraph-level text decorations (or null if none) private Brush _defaultTextDecorationsBrush; // Default brush for paragraph text decorations [Flags] private enum StatusFlags { None = 0, IsDisposed = 0x00000001, HasOverflowed = 0x00000002, BoundingBoxComputed = 0x00000004, RightToLeft = 0x00000008, HasCollapsed = 0x00000010, KeepState = 0x00000020, IsTruncated = 0x00000040, } private enum CaretDirection { Forward, Backward, Backspace } /// /// Constructing a FullTextLine /// /// text formatting settings /// Line's first cp /// character length of the line /// paragraph width /// line formatting control flags internal FullTextLine( FormatSettings settings, int cpFirst, int lineLength, int paragraphWidth, LineFlags lineFlags ) : this() { if ( (lineFlags & LineFlags.KeepState) != 0 || settings.Pap.AlwaysCollapsible) { _statusFlags |= StatusFlags.KeepState; } int finiteFormatWidth = settings.GetFiniteFormatWidth(paragraphWidth); FullTextState fullText = FullTextState.Create(settings, cpFirst, finiteFormatWidth); // formatting the line FormatLine( fullText, cpFirst, lineLength, fullText.FormatWidth, finiteFormatWidth, paragraphWidth, lineFlags, null // collapsingSymbol ); } ////// Finalizing full text line /// ~FullTextLine() { DisposeInternal(true); } ////// Releasing the line's unmanaged resource /// public override void Dispose() { DisposeInternal(false); GC.SuppressFinalize(this); } ////// Disposing LS unmanaged memory for text line /// ////// Critical - as this calls LoFinalizeLine, LoDestroyLine and _ploline.Value which /// are all critical functions. /// Safe - as this does not take any pointer parameters that it passes directly to /// the Critical functions. _ploline.Value is critical for set. /// [SecurityCritical, SecurityTreatAsSafe] private void DisposeInternal(bool finalizing) { if (_ploline.Value != System.IntPtr.Zero) { UnsafeNativeMethods.LoDisposeLine(_ploline.Value, finalizing); _ploline.Value = System.IntPtr.Zero; GC.KeepAlive(this); } } ////// Empty private constructor /// ////// Critical - as this calls the constructor for SecurityCriticalDataForSet. /// Safe - as this just initializes it with the default value. /// [SecurityCritical, SecurityTreatAsSafe] private FullTextLine() { _metrics = new TextMetrics(); _ploline = new SecurityCriticalDataForSet(IntPtr.Zero); } /// /// format text line using LS /// /// state of the full text backing store /// first cp to format /// character length of the line /// width used to format /// width used to detect overflowing of format result /// paragraph width /// line formatting control flags /// line end collapsing symbol ////// Critical - as this calls the setter for _ploline.Value which is type SecurityCriticalDataForSet. /// _ploc is critical for set as it's required to properly match the LS context used to /// format the line during display time. /// Safe - as this doesn't get set to a random parameter passed in but rather to a value returned /// by a safe function TextFormatterContext.CreateLine(). /// [SecurityCritical, SecurityTreatAsSafe] private void FormatLine( FullTextState fullText, int cpFirst, int lineLength, int formatWidth, int finiteFormatWidth, int paragraphWidth, LineFlags lineFlags, FormattedTextSymbols collapsingSymbol ) { _metrics._formatter = fullText.Formatter; Debug.Assert(_metrics._formatter != null); TextStore store = fullText.TextStore; TextStore markerStore = fullText.TextMarkerStore; FormatSettings settings = store.Settings; ParaProp pap = settings.Pap; _paragraphTextDecorations = pap.TextDecorations; if (_paragraphTextDecorations != null) { if (_paragraphTextDecorations.Count != 0) { _defaultTextDecorationsBrush = pap.DefaultTextDecorationsBrush; } else { _paragraphTextDecorations = null; } } // acquiring LS context TextFormatterContext context = _metrics._formatter.AcquireContext(fullText, IntPtr.Zero); LsLInfo plslineInfo = new LsLInfo(); LsLineWidths lineWidths = new LsLineWidths(); fullText.SetTabs(context); int lscpLineLength = 0; // line length in LSCP if (lineLength > 0) { // line length is previously known (e.g. during optimal paragraph formatting), // prefetch lsruns up to the specified line length. lscpLineLength = PrefetchLSRuns(store, cpFirst, lineLength); } IntPtr ploline; LsErr lserr = context.CreateLine( cpFirst, lscpLineLength, formatWidth, lineFlags, IntPtr.Zero, // single-line formatting does not require break record out ploline, out plslineInfo, out _depthQueryMax, out lineWidths ); // Did we exceed the LineServices maximum line width? if (lserr == LsErr.TooLongParagraph) { // Determine where to insert a fake line break. FullTextState.CpMeasured // is a reasonable estimate since we know the nominal widths up to that // point fit within the margin. int cpLimit = fullText.CpMeasured; int subtract = 1; for (;;) { // The line must contain at least one character position. if (cpLimit < 1) { cpLimit = 1; } store.InsertFakeLineBreak(cpLimit); lserr = context.CreateLine( cpFirst, lscpLineLength, formatWidth, lineFlags, IntPtr.Zero, // single-line formatting does not require break record out ploline, out plslineInfo, out _depthQueryMax, out lineWidths ); if (lserr != LsErr.TooLongParagraph || cpLimit == 1) { // We're done or can't chop off any more text. break; } else { // Chop off more text and try again. Double the amount of // text we chop off each time so we retry too many times. cpLimit = fullText.CpMeasured - subtract; subtract *= 2; } } } _ploline.Value = ploline; // get the exception in context before it is released Exception callbackException = context.CallbackException; // release the context context.Release(); if(lserr != LsErr.None) { GC.SuppressFinalize(this); if(callbackException != null) { // rethrow exception thrown in callbacks throw WrapException(callbackException); } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.CreateLineFailure, lserr), lserr); } } // keep context alive at least till here GC.KeepAlive(context); unsafe { // construct text metrics for the line _metrics.Compute( fullText, cpFirst, paragraphWidth, collapsingSymbol, ref lineWidths, &plslineInfo ); } // keep record for min width as we may be formatting min/max _textMinWidthAtTrailing = lineWidths.upMinStartTrailing - _metrics._textStart; if (collapsingSymbol != null) { _collapsingSymbol = collapsingSymbol; _textMinWidthAtTrailing += _metrics._formatter.RealToIdeal(collapsingSymbol.Width); } else { // overflow detection for potential collapsible line if (_metrics._textStart + _metrics._textWidthAtTrailing > finiteFormatWidth) { // line has overflowed _statusFlags |= StatusFlags.HasOverflowed; // let's keep the full text state around. We'll need it later for collapsing _fullText = fullText; } } if ( fullText != null && ( fullText.KeepState || (_statusFlags & StatusFlags.KeepState) != 0 ) ) { // the state of full text is to be kept after formatting is done _fullText = fullText; } // retain all line properties for interactive operations _ploc = context.Ploc; _cpFirst = cpFirst; _paragraphWidth = paragraphWidth; if (pap.RightToLeft) _statusFlags |= StatusFlags.RightToLeft; if (plslineInfo.fForcedBreak != 0) _statusFlags |= StatusFlags.IsTruncated; // retain the state of plsruns _plsrunVector = store.PlsrunVector; _lsrunsMainText = store.LsrunList; if (markerStore != null) _lsrunsMarkerText = markerStore.LsrunList; // we store the text source in the line in case drawing code calls // the TextSource to find out the text effect index. // _textSource = settings.TextSource; } ////// Wraps a caught exception in a new exception object of the same type, if possible. /// Otherwise just return the original exception. /// private static Exception WrapException(Exception caughtException) { // We're going to try to create a new exception of the same type as caughtException. Type t = caughtException.GetType(); // Make sure the type is public to avoid MethodAccessException in partial trust. if (t.IsPublic) { // Look for a public instance constructor with signature ctor(Exception) ConstructorInfo constructor = t.GetConstructor( new Type[] { typeof(Exception) } ); if (constructor != null) { return (Exception)constructor.Invoke( new object[] { caughtException } ); } // Look for a public instance constructor with signature ctor(string,Exception) constructor = t.GetConstructor( new Type[] { typeof(string), typeof(Exception) } ); if (constructor != null) { return (Exception)constructor.Invoke( new object[] { caughtException.Message, caughtException } ); } } // We couldn't find an appropriate constructor so fall back to returning the original // exception object. We don't want to wrap the exception in some arbitrary exception type // because the client may have thrown the exception and may want to catch it higher up // the stack. // // The disadvantage of throwing the same object again is the original stack is lost. This // makes debugging harder (partially mitigated by the stack trace string available via // the Data property -- but only in full dumps), and means Watson errors will be bucketized // only based on the current stack, i.e., FormatLine. Hopefully this case will be rare. // return caughtException; } ////// Append line end collapsing symbol /// private void AppendCollapsingSymbol( FormattedTextSymbols symbol ) { Debug.Assert(_collapsingSymbol == null && symbol != null); _collapsingSymbol = symbol; int symbolIdealWidth = _metrics._formatter.RealToIdeal(symbol.Width); _metrics.AppendCollapsingSymbolWidth(symbolIdealWidth); _textMinWidthAtTrailing += symbolIdealWidth; } ////// Prefetch the lsruns up to the point of the specified line length and map /// the specified length to the corresponding LSCP length. /// ////// See comment in the remark section of FullTextState.GetBreakpointInternalCp. /// private int PrefetchLSRuns( TextStore store, int cpFirst, int lineLength ) { Debug.Assert(lineLength > 0); LSRun lsrun; int prefetchLength = 0; int lscpLineLength = 0; int lastSpanLength = 0; int lastRunLength = 0; do { Plsrun plsrun; int lsrunOffset; int lsrunLength; lsrun = store.FetchLSRun( cpFirst + lscpLineLength, out plsrun, out lsrunOffset, out lsrunLength ); if (lineLength == prefetchLength && lsrun.Type == Plsrun.Reverse) { break; } lastSpanLength = lsrunLength; lastRunLength = lsrun.Length; lscpLineLength += lastSpanLength; prefetchLength += lastRunLength; } while ( !TextStore.IsNewline(lsrun.Type) && lineLength >= prefetchLength ); // calibrate the LSCP length to the LSCP equivalence of the last CP of the line if (prefetchLength == lineLength || lastSpanLength == lastRunLength) return lscpLineLength - prefetchLength + lineLength; Invariant.Assert(prefetchLength - lineLength == lastRunLength); return lscpLineLength - lastSpanLength; } ////// Draw line /// /// drawing context /// drawing origin /// indicate the inversion of the drawing surface public override void Draw( DrawingContext drawingContext, Point origin, InvertAxes inversion ) { if (drawingContext == null) { throw new ArgumentNullException("drawingContext"); } if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } MatrixTransform antiInversion = TextFormatterImp.CreateAntiInversionTransform( inversion, _metrics._formatter.IdealToReal(_paragraphWidth), _metrics._formatter.IdealToReal(_metrics._height) ); if (antiInversion == null) { DrawTextLine(drawingContext, origin, null); } else { // Apply anti-inversion transform to correct the visual drawingContext.PushTransform(antiInversion); try { DrawTextLine(drawingContext, origin, antiInversion); } finally { drawingContext.Pop(); } } } ////// Draw complex text line /// /// drawing surface /// offset to the line origin /// anti-inversion transform applied on the surface ////// Critical - as this calls Critical function LoDisplayLine. /// Safe - as this doesn't take any pointer parameters that are passed to /// LoDisplayLine directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void DrawTextLine( DrawingContext drawingContext, Point origin, MatrixTransform antiInversion ) { Rect boundingBox = Rect.Empty; if (_ploline.Value != System.IntPtr.Zero) { TextFormatterContext context; LsErr lserr = LsErr.None; LSRECT rect = new LSRECT(0, 0, _metrics._textWidthAtTrailing, _metrics._height); // DrawingState needs to be properly disposed after performing actual drawing operations. using (DrawingState drawingState = new DrawingState(drawingContext, origin, antiInversion, this)) { context = _metrics._formatter.AcquireContext( drawingState, _ploc.Value ); // set the collector and send the line to LS to draw context.EmptyBoundingBox(); // LS line reference origin LSPOINT lsRefOrigin = new LSPOINT(0, _metrics._baselineOffset); lserr = UnsafeNativeMethods.LoDisplayLine( _ploline.Value, ref lsRefOrigin, 1, // 0 - opaque, 1 - transparent ref rect ); } boundingBox = context.BoundingBox; // get the exception in context before it is released Exception callbackException = context.CallbackException; context.Release(); if(lserr != LsErr.None) { if(callbackException != null) { // rethrow exception thrown in callbacks throw callbackException; } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.CreateLineFailure, lserr), lserr); } } // keep context alive at least til here GC.KeepAlive(context); } if (_collapsingSymbol != null) { // draw collapsing symbol if any Point vectorToOrigin = new Point(); if (antiInversion != null) { vectorToOrigin = origin; origin.X = origin.Y = 0; } boundingBox.Union(DrawCollapsingSymbol(drawingContext, origin, vectorToOrigin)); } BuildOverhang(origin, boundingBox); _statusFlags |= StatusFlags.BoundingBoxComputed; } ////// Draw line end collapsing symbol /// private Rect DrawCollapsingSymbol( DrawingContext drawingContext, Point lineOrigin, Point vectorToLineOrigin ) { int symbolIdealWidth = _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); Point symbolOrigin = LSRun.UVToXY( lineOrigin, vectorToLineOrigin, LSLineUToParagraphU(_metrics._textStart + _metrics._textWidthAtTrailing - symbolIdealWidth), _metrics._baselineOffset, _metrics._formatter.ToReal, this ); return _collapsingSymbol.Draw(drawingContext, symbolOrigin); } ////// Make sure the bounding box is calculated /// private void CheckBoundingBox() { if ((_statusFlags & StatusFlags.BoundingBoxComputed) == 0) { DrawTextLine(null, new Point(0, 0), null); } Debug.Assert((_statusFlags & StatusFlags.BoundingBoxComputed) != 0); } ////// Client to collapse the line to fit for display /// /// a list of collapsing properties public override TextLine Collapse( params TextCollapsingProperties[] collapsingPropertiesList ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if ( !HasOverflowed && (_statusFlags & StatusFlags.KeepState) == 0) { // Attempt to collapse a non-overflowed line results in the original line returned return this; } if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) throw new ArgumentNullException("collapsingPropertiesList"); TextCollapsingProperties collapsingProp = collapsingPropertiesList[0]; double constraintWidth = collapsingProp.Width; if (constraintWidth >= Width) { // constraining width is greater than original line width, no collapsing neeeded. return this; } FormattedTextSymbols symbol = null; if (collapsingProp.Symbol != null) { // create formatted collapsing symbol symbol = new FormattedTextSymbols( _metrics._formatter.GlyphingCache, collapsingProp.Symbol, RightToLeft, _metrics._formatter.ToIdeal ); constraintWidth -= symbol.Width; } Debug.Assert(_fullText != null); FullTextLine line = new TextMetrics.FullTextLine(); // collapsing preserves original line metrics Debug.Assert(_metrics._height > 0); line._metrics._formatter = _metrics._formatter; line._metrics._height = _metrics._height; line._metrics._baselineOffset = _metrics._baselineOffset; if (constraintWidth > 0) { // format main text line with constraint width int finiteFormatWidth = _fullText.TextStore.Settings.GetFiniteFormatWidth( _metrics._formatter.RealToIdeal(constraintWidth) ); bool forceWrap = _fullText.ForceWrap; _fullText.ForceWrap = true; if ((_statusFlags & StatusFlags.KeepState) != 0) { // inherit this flag so the collapsed line retains full text state too. line._statusFlags |= StatusFlags.KeepState; } line.FormatLine( _fullText, _cpFirst, 0, // no line length limit finiteFormatWidth, finiteFormatWidth, _paragraphWidth, // collapsed line is still bound to the original paragraph width (collapsingProp.Style == TextCollapsingStyle.TrailingCharacter ? LineFlags.BreakAlways : LineFlags.None), symbol ); _fullText.ForceWrap = forceWrap; line._metrics._cchDepend = 0; // no dependency } else if (symbol != null) { line.AppendCollapsingSymbol(symbol); } if (line._metrics._cchLength < Length) { line._collapsedRange = new TextCollapsedRange( _cpFirst + line._metrics._cchLength, Length - line._metrics._cchLength, Width - line.Width ); // collapsed line has the original length line._metrics._cchLength = Length; } // mark the indication flags signify collapsing line._statusFlags |= StatusFlags.HasCollapsed; line._statusFlags &= ~StatusFlags.HasOverflowed; return line; } ////// Client to get a collection of collapsed cha----r ranges after a line has been collapsed /// public override IListGetTextCollapsedRanges() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if (_collapsedRange == null) return null; Debug.Assert(HasCollapsed); return new TextCollapsedRange[] { _collapsedRange }; } /// /// Client to get the character hit corresponding to the specified /// distance from the beginning of the line. /// /// distance in text flow direction from the beginning of the line ///character hit public override CharacterHit GetCharacterHitFromDistance( double distance ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } return CharacterHitFromDistance(ParagraphUToLSLineU(_metrics._formatter.RealToIdeal(distance))); } ////// Get character hit from specified hittest distance relative to line start /// private CharacterHit CharacterHitFromDistance(int hitTestDistance) { // assuming the first cp of the line CharacterHit characterHit = new CharacterHit(_cpFirst, 0); if(_ploline.Value == IntPtr.Zero) { // Returning the first cp for the empty line return characterHit; } if ( HasCollapsed && _collapsedRange != null && _collapsingSymbol != null ) { int lineEndDistance = _metrics._textStart + _metrics._textWidthAtTrailing; int rangeWidth = _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); if (hitTestDistance >= lineEndDistance - rangeWidth) { if (lineEndDistance - hitTestDistance < rangeWidth / 2) { // The hit-test distance is within the trailing edge of the collapsed range, // return the character hit at the beginning of the range. return new CharacterHit(_collapsedRange.TextSourceCharacterIndex, _collapsedRange.Length); } // The hit-test distance is within the leading edge of the collapsed range, // return the character hit at the beginning of the range. return new CharacterHit(_collapsedRange.TextSourceCharacterIndex, 0); } } LsTextCell lsTextCell; LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; int actualSublineCount; QueryLinePointPcp( new Point(hitTestDistance, 0), sublineInfo, out actualSublineCount, out lsTextCell ); if (actualSublineCount > 0 && lsTextCell.dupCell > 0) { // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); // Assuming caret stops at every codepoint. // // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. int caretStopCount = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; int codepointsToNextCaretStop = lsrun.IsHitTestable ? 1 : lsrun.Length; if ( lsrun.IsHitTestable && ( lsrun.HasExtendedCharacter || lsrun.NeedsCaretInfo) ) { // A hit-testable run with caret stops at every cluster boundaries, // e.g. run with combining mark, with extended characters or complex scripts such as Thai codepointsToNextCaretStop = caretStopCount; caretStopCount = 1; } // All the UV coordinate in subline are in main direction. If the last subline where // we hittest runs in the opposite direction, the logical advance from text cell start cp // will be negative value. int direction = (sublineInfo[actualSublineCount - 1].lstflowSubLine == sublineInfo[0].lstflowSubLine) ? 1 : -1; hitTestDistance = (hitTestDistance - lsTextCell.pointUvStartCell.x) * direction; Invariant.Assert(caretStopCount > 0); int wholeAdvance = lsTextCell.dupCell / caretStopCount; int remainingAdvance = lsTextCell.dupCell % caretStopCount; for (int i = 0; i < caretStopCount; i++) { int caretAdvance = wholeAdvance; if (remainingAdvance > 0) { caretAdvance++; remainingAdvance--; } if (hitTestDistance <= caretAdvance) { if (hitTestDistance > caretAdvance / 2) { // hittest at the trailing edge of the current caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + i, codepointsToNextCaretStop); } // hittest at the leading edge of the current caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + i, 0); } hitTestDistance -= caretAdvance; } // hittest beyond the last caret stop, return the trailing edge of the last caret stop return new CharacterHit(GetExternalCp(lsTextCell.lscpStartCell) + caretStopCount - 1, codepointsToNextCaretStop); } return characterHit; } ////// Client to get the distance from the beginning of the line from the specified character hit. /// /// index to text source's character store ///distance in text flow direction from the beginning of the line. public override double GetDistanceFromCharacterHit( CharacterHit characterHit ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); return _metrics._formatter.IdealToReal(LSLineUToParagraphU(DistanceFromCharacterHit(characterHit))); } ////// Get hittest distance relative to line start from specified character hit /// private int DistanceFromCharacterHit(CharacterHit characterHit) { int hitTestDistance = 0; if (_ploline.Value == IntPtr.Zero) { // Returning start of the line for empty line return hitTestDistance; } if (characterHit.FirstCharacterIndex >= _cpFirst + _metrics._cchLength) { // Returning line width for character hit beyond the last caret stop return _metrics._textStart + _metrics._textWidthAtTrailing; } if ( HasCollapsed && _collapsedRange != null && characterHit.FirstCharacterIndex >= _collapsedRange.TextSourceCharacterIndex ) { // The current character hit is beyond the beginning of the collapsed range int lineEndDistance = _metrics._textStart + _metrics._textWidthAtTrailing; if ( characterHit.FirstCharacterIndex >= _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length || characterHit.TrailingLength != 0 || _collapsingSymbol == null ) { // The current character hit either hits outside, // or it's at the trailing edge of the collapsed range return lineEndDistance; } return lineEndDistance - _metrics._formatter.RealToIdeal(_collapsingSymbol.Width); } int actualSublineCount; LsTextCell lsTextCell; LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; int lscpCurrent = GetInternalCp(characterHit.FirstCharacterIndex); QueryLineCpPpoint( lscpCurrent, sublineInfo, out actualSublineCount, out lsTextCell ); if (actualSublineCount > 0) { return lsTextCell.pointUvStartCell.x + GetDistanceInsideTextCell( lscpCurrent, characterHit.TrailingLength != 0, sublineInfo, actualSublineCount, ref lsTextCell ); } return hitTestDistance; } ////// Get distance from the start of text cell to the specified lscp /// private int GetDistanceInsideTextCell( int lscpCurrent, bool isTrailing, LsQSubInfo[] sublineInfo, int actualSublineCount, ref LsTextCell lsTextCell ) { int distanceInCell = 0; // All the UV coordinate in subline are in main direction. If the last subline where // we hittest runs in the opposite direction, the logical advance from text cell start cp // will be negative value. int direction = (sublineInfo[actualSublineCount - 1].lstflowSubLine == sublineInfo[0].lstflowSubLine) ? 1 : -1; // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. // // Assuming caret stops at every codepoint in the run. int caretStopCount = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; int codepointsFromStartCell = lscpCurrent - lsTextCell.lscpStartCell; // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); if ( lsrun.IsHitTestable && ( lsrun.HasExtendedCharacter || lsrun.NeedsCaretInfo) ) { // A hit-testable run with caret stops at every cluster boundaries, // e.g. run with combining mark, with extended characters or complex scripts such as Thai caretStopCount = 1; } Invariant.Assert(caretStopCount > 0); int wholeAdvance = lsTextCell.dupCell / caretStopCount; int remainingAdvance = lsTextCell.dupCell % caretStopCount; for (int i = 1; i <= caretStopCount; i++) { int caretAdvance = wholeAdvance; if (remainingAdvance > 0) { caretAdvance++; remainingAdvance--; } if (codepointsFromStartCell < i) { if (isTrailing) { // hit-test at the trailing edge of the current caret stop, include the current caret advance return (distanceInCell + caretAdvance) * direction; } // hit-test at the leading edge of the current caret stop, return the accumulated distance return distanceInCell * direction; } distanceInCell += caretAdvance; } // hit-test beyond the last caret stop, return the total accumated distance up to the trailing edge of the last caret stop. return distanceInCell * direction; } ////// Client to get the next character hit for caret navigation /// /// the current character hit ///the next character hit public override CharacterHit GetNextCaretCharacterHit( CharacterHit characterHit ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); if (_ploline.Value == System.IntPtr.Zero) { return characterHit; } int caretStopIndex; int offsetToNextCaretStopIndex; bool found = GetNextOrPreviousCaretStop( characterHit.FirstCharacterIndex, CaretDirection.Forward, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is beyond the last caret stop. return characterHit; } if (caretStopIndex <= characterHit.FirstCharacterIndex && characterHit.TrailingLength != 0) { // We treat trailing length of the current character hit as a flag on the way in. // A non-zero value indicates that it is on the trailing edge of the current // caret stop. At this point, the current caret stop fully encloses the input index, // and the input is at the trailing edge. In this case, we move it to the trailing // edge of the next caret stop. found = GetNextOrPreviousCaretStop( caretStopIndex + offsetToNextCaretStopIndex, CaretDirection.Forward, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // This current index is beyond the last caret stop return characterHit; } return new CharacterHit(caretStopIndex, offsetToNextCaretStopIndex); } // If the current character hit is at the leading edge, // move it to trailing edge of the current caret stop. return new CharacterHit(caretStopIndex, offsetToNextCaretStopIndex); } ////// Client to get the previous character hit for caret navigation /// /// the current character hit ///the previous character hit public override CharacterHit GetPreviousCaretCharacterHit( CharacterHit characterHit ) { return GetPreviousCaretCharacterHitByBehavior(characterHit, CaretDirection.Backward); } ////// Client to get the previous character hit after backspacing /// /// the current character hit ///the character hit after backspacing public override CharacterHit GetBackspaceCaretCharacterHit( CharacterHit characterHit ) { return GetPreviousCaretCharacterHitByBehavior(characterHit, CaretDirection.Backspace); } ////// Calculate previous caret character hit based on the caret action /// private CharacterHit GetPreviousCaretCharacterHitByBehavior( CharacterHit characterHit, CaretDirection direction ) { Debug.Assert(direction == CaretDirection.Backward || direction == CaretDirection.Backspace); if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } TextFormatterImp.VerifyCaretCharacterHit(characterHit, _cpFirst, _metrics._cchLength); if (_ploline.Value == IntPtr.Zero) { return characterHit; } if ( characterHit.FirstCharacterIndex == _cpFirst && characterHit.TrailingLength == 0) { // We are already at the beginning of the line return characterHit; } int caretStopIndex; int offsetToNextCaretStopIndex; bool found = GetNextOrPreviousCaretStop( characterHit.FirstCharacterIndex, direction, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is before the 1st caret stop. return characterHit; } if ( offsetToNextCaretStopIndex != 0 && characterHit.TrailingLength == 0 && caretStopIndex != _cpFirst && caretStopIndex >= characterHit.FirstCharacterIndex ) { // If the current character hit is at the leading edge and it is not at the first caret stop, // move it to leading edge of the previous caret stop. At this point, the current character stop // fully encloses the input index and the input is at the leading edge. found = GetNextOrPreviousCaretStop( caretStopIndex - 1, // position at the character immediately preceding the current caret stop direction, out caretStopIndex, out offsetToNextCaretStopIndex ); if (!found) { // The current index is before the 1st caret stop. return characterHit; } } // The current chracter hit is either beyond the last caret stop, // or it's at the trailing edge of the current caret stop, // or the current index is at the leading edge of the first caret stop. // // In such cases, move to the leading edge of the closest caret stop. return new CharacterHit(caretStopIndex, 0); } ////// Given a specified current character index, calculate the character index /// to the closest caret stop before or at the current index; and the number /// of codepoints from the closest caret stop to the next caret stop. /// private bool GetNextOrPreviousCaretStop( int currentIndex, CaretDirection direction, out int caretStopIndex, out int offsetToNextCaretStopIndex ) { caretStopIndex = currentIndex; offsetToNextCaretStopIndex = 0; if ( HasCollapsed && _collapsedRange != null && currentIndex >= _collapsedRange.TextSourceCharacterIndex ) { // current index is within collapsed range, caretStopIndex = _collapsedRange.TextSourceCharacterIndex; if (currentIndex < _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length) offsetToNextCaretStopIndex = _collapsedRange.Length; return true; } LsQSubInfo[] sublineInfo = new LsQSubInfo[_depthQueryMax]; LsTextCell lsTextCell = new LsTextCell(); int lscpVisisble = GetInternalCp(currentIndex); bool found = FindNextOrPreviousVisibleCp(lscpVisisble, direction, out lscpVisisble); if (!found) { return false; // there is no caret stop anymore in the given direction. } int actualSublineCount; QueryLineCpPpoint( lscpVisisble, sublineInfo, out actualSublineCount, out lsTextCell ); // Locate the current caret stop caretStopIndex = GetExternalCp(lsTextCell.lscpStartCell); if ( actualSublineCount > 0 && lscpVisisble >= lsTextCell.lscpStartCell && lscpVisisble <= lsTextCell.lscpEndCell ) { // the last subline contains the run that owns the querying lscp LSRun lsrun = GetRun((Plsrun)sublineInfo[actualSublineCount - 1].plsrun); if (lsrun.IsHitTestable) { if ( lsrun.HasExtendedCharacter || (direction != CaretDirection.Backspace && lsrun.NeedsCaretInfo) ) { // LsTextCell.lscpEndCell is the index to the last lscp still in the cell. // The number of LSCP within the text cell is equal to the number of CP. offsetToNextCaretStopIndex = lsTextCell.lscpEndCell + 1 - lsTextCell.lscpStartCell; } else { // caret stops before every codepoint caretStopIndex = GetExternalCp(lscpVisisble); offsetToNextCaretStopIndex = 1; } } else { // run is not hit-testable, caret navigation is not allowed in the run, // the next caret stop is therefore either at the end of the run or the end of the line whichever reached first. offsetToNextCaretStopIndex = Math.Min(Length, lsrun.Length - caretStopIndex + lsrun.OffsetToFirstCp + _cpFirst); } } return true; } ////// Search from the given lscp (inclusive) towards the specified direction for the /// closest navigable cp. Return true is one such cp is found, false otherwise. /// private bool FindNextOrPreviousVisibleCp( int lscp, CaretDirection direction, out int lscpVisisble ) { lscpVisisble = lscp; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); if (direction == CaretDirection.Forward) { while (lscpVisisble < _metrics._lscpLim) { plsrunSpanRider.At(lscpVisisble - _cpFirst); LSRun run = GetRun((Plsrun) plsrunSpanRider.CurrentElement); // When scanning forward, only trailine edges of visiable content are navigable. if (run.IsVisible) { return true; } lscpVisisble += plsrunSpanRider.Length; // move to start of next span } } else { Debug.Assert(direction == CaretDirection.Backward || direction == CaretDirection.Backspace); // lscpCurrent can be right after the end of the line, we snap it back to be at the end of the line. lscpVisisble = Math.Min(lscpVisisble, _metrics._lscpLim - 1); while (lscpVisisble >= _cpFirst) { plsrunSpanRider.At(lscpVisisble - _cpFirst); LSRun run = GetRun((Plsrun) plsrunSpanRider.CurrentElement); // When scanning backward, visiable content has caret stop at its leading edge. if (run.IsVisible) { return true; } // When scanning backward, the newline sequence has caret stop at its leading edge. if (run.IsNewline) { // set navigable cp at the start of newline sequence. lscpVisisble = _cpFirst + plsrunSpanRider.CurrentSpanStart; return true; } lscpVisisble = _cpFirst + plsrunSpanRider.CurrentSpanStart - 1; // move to the end of previous span } } lscpVisisble = lscp; return false; } ////// Create zerowidth bounds with line height /// private TextBounds[] CreateDegenerateBounds() { return new TextBounds[] { new TextBounds( new Rect(0, 0, 0, Height), (RightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight), null // runBounds ) }; } ////// Create bounds of collapsing symbol /// private TextBounds CreateCollapsingSymbolBounds() { Debug.Assert(_collapsingSymbol != null); return new TextBounds( new Rect(Start + Width - _collapsingSymbol.Width, 0, _collapsingSymbol.Width, Height), (RightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight), null ); } ////// Client to get an array of bounding rectangles of a range of characters within a text line. /// /// index of first character of specified range /// number of characters of the specified range ///an array of bounding rectangles. public override IListGetTextBounds( int firstTextSourceCharacterIndex, int textLength ) { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if(textLength == 0) { throw new ArgumentOutOfRangeException("textLength", SR.Get(SRID.ParameterMustBeGreaterThanZero)); } if(textLength < 0) { firstTextSourceCharacterIndex += textLength; textLength = -textLength; } if(firstTextSourceCharacterIndex < _cpFirst) { textLength += (firstTextSourceCharacterIndex - _cpFirst); firstTextSourceCharacterIndex = _cpFirst; } if(firstTextSourceCharacterIndex > _cpFirst + _metrics._cchLength - textLength) { textLength = (_cpFirst + _metrics._cchLength - firstTextSourceCharacterIndex); } if (_ploline.Value == IntPtr.Zero) { return CreateDegenerateBounds(); } Point position = new Point(0,0); // get first cp sublines & text cell int firstDepth; LsTextCell firstTextCell; LsQSubInfo[] firstSublines = new LsQSubInfo[_depthQueryMax]; int lscpFirst = GetInternalCp(firstTextSourceCharacterIndex); QueryLineCpPpoint( lscpFirst, firstSublines, out firstDepth, out firstTextCell ); if(firstDepth <= 0) { // this happens for empty line (line containing only EOP) return CreateDegenerateBounds(); } // get last cp sublines & text cell int lastDepth; LsTextCell lastTextCell; LsQSubInfo[] lastSublines = new LsQSubInfo[_depthQueryMax]; int lscpEnd = GetInternalCp(firstTextSourceCharacterIndex + textLength - 1); QueryLineCpPpoint( lscpEnd, lastSublines, out lastDepth, out lastTextCell ); if(lastDepth <= 0) { // This should never happen but if it does, we still cant throw here. // We must return something even though it's a degenerate bounds or // client hittesting code will just crash. Debug.Assert(false); return CreateDegenerateBounds(); } // check if collapsing symbol is wholely selected bool collapsingSymbolSelected = ( _collapsingSymbol != null && _collapsedRange != null && firstTextSourceCharacterIndex < _collapsedRange.TextSourceCharacterIndex && firstTextSourceCharacterIndex + textLength - _collapsedRange.TextSourceCharacterIndex > _collapsedRange.Length / 2 ); TextBounds[] bounds = null; ArrayList boundsList = null; // By default, if the hittested CP is visible, then we want cpFirst to hit // on the leading edge of the first visible cp, and cpEnd to hit on the trailing edge of the // last visible cp. bool isCpFirstTrailing = false; bool isCpEndTrailing = true; if (lscpFirst > firstTextCell.lscpEndCell) { // when cpFirst is after the last visible cp, then it hits the trailing edge of that cp isCpFirstTrailing = true; } if (lscpEnd < lastTextCell.lscpStartCell) { // when cpEnd is before the first visible cp, then it hits the leading edge of that cp isCpEndTrailing = false; } if (firstDepth == lastDepth && firstSublines[firstDepth - 1].lscpFirstSubLine == lastSublines[lastDepth - 1].lscpFirstSubLine) { // first and last cp are within the same subline int count = collapsingSymbolSelected ? 2 : 1; bounds = new TextBounds[count]; bounds[0] = new TextBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x ), 0 ), new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpEnd, isCpEndTrailing, lastSublines, lastDepth, ref lastTextCell ) + lastTextCell.pointUvStartCell.x ), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(firstSublines[firstDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpFirst, lscpEnd + 1) ); if (count > 1) { bounds[1] = CreateCollapsingSymbolBounds(); } } else { // first and last cp are not in the same subline. boundsList = new ArrayList(2); int lscpCurrent = lscpFirst; // The hittested cp can be outside of the returned sublines when it is a hidden cp. // We should not pass beyond the end of the returned sublines. int lscpEndInSubline = Math.Min( lscpEnd, lastSublines[lastDepth - 1].lscpFirstSubLine + lastSublines[lastDepth - 1].lsdcpSubLine - 1 ); int currentDistance = GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x; int baseLevelDepth; CollectTextBoundsToBaseLevel( boundsList, ref lscpCurrent, ref currentDistance, firstSublines, firstDepth, lscpEndInSubline, out baseLevelDepth ); if (baseLevelDepth < lastDepth) { CollectTextBoundsFromBaseLevel( boundsList, ref lscpCurrent, ref currentDistance, lastSublines, lastDepth, baseLevelDepth ); } // Collect the bounds from the start of the immediate enclosing subline of the last LSCP // to the hittested text cell. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU(currentDistance), 0 ), new LSPOINT( LSLineUToParagraphU( GetDistanceInsideTextCell( lscpEnd, isCpEndTrailing, lastSublines, lastDepth, ref lastTextCell ) + lastTextCell.pointUvStartCell.x ), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(lastSublines[lastDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, lscpEnd + 1) ) ); } if (bounds == null) { Debug.Assert(boundsList != null); if (boundsList.Count > 0) { if (collapsingSymbolSelected) { // add one more for collapsed symbol AddValidTextBounds(boundsList, CreateCollapsingSymbolBounds()); } bounds = new TextBounds[boundsList.Count]; for (int i = 0; i < boundsList.Count; i++) { bounds[i] = (TextBounds)boundsList[i]; } } else { // No non-zerowidth bounds detected, fallback to the position of first cp // This can happen if hidden run is hittest'd. int u = LSLineUToParagraphU( GetDistanceInsideTextCell( lscpFirst, isCpFirstTrailing, firstSublines, firstDepth, ref firstTextCell ) + firstTextCell.pointUvStartCell.x ); bounds = new TextBounds[] { new TextBounds( LSRun.RectUV( position, new LSPOINT(u, 0), new LSPOINT(u, _metrics._height), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(firstSublines[firstDepth - 1].lstflowSubLine), null ) }; } } return bounds; } /// /// Base level is the highest subline's level that both sets of sublines have in common. /// This method starts collecting text bounds at the specified LSCP. The first bounds being /// collected is the one from the LSCP to the end of its immediate enclosing subline. The /// subsequent bounds are from the end of run to the end of subline of the lower level until /// it reaches the base level. /// private void CollectTextBoundsToBaseLevel( ArrayList boundsList, ref int lscpCurrent, ref int currentDistance, LsQSubInfo[] sublines, int sublineDepth, int lscpEnd, out int baseLevelDepth ) { baseLevelDepth = sublineDepth; if (lscpEnd < sublines[sublineDepth - 1].lscpFirstSubLine + sublines[sublineDepth - 1].lsdcpSubLine) { // The immedidate enclosing subline already contains the lscp end. It means we are already // at base level. return; } // Collect text bounds from the current lscp to the end of the immediate enclosing subline. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(currentDistance), 0), new LSPOINT( LSLineUToParagraphU(GetEndOfSublineDistance(sublines, sublineDepth - 1)), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[sublineDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, sublines[sublineDepth - 1].lscpFirstSubLine + sublines[sublineDepth - 1].lsdcpSubLine) ) ); // Collect text bounds from end of run to the end of subline at lower levels until we reach the // common level. We reach common level when the subline at that level contains the lscpEnd. for ( baseLevelDepth = sublineDepth - 1; baseLevelDepth > 0 && (lscpEnd >= sublines[baseLevelDepth - 1].lscpFirstSubLine + sublines[baseLevelDepth - 1].lsdcpSubLine); baseLevelDepth-- ) { int sublineIndex = baseLevelDepth - 1; AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT( LSLineUToParagraphU(GetEndOfRunDistance(sublines, sublineIndex)), 0 ), new LSPOINT( LSLineUToParagraphU(GetEndOfSublineDistance(sublines, sublineIndex)), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[sublineIndex].lstflowSubLine), CalculateTextRunBounds( sublines[sublineIndex].lscpFirstRun + sublines[sublineIndex].lsdcpRun, sublines[sublineIndex].lscpFirstSubLine + sublines[sublineIndex].lsdcpSubLine ) ) ); } // base level depth must be at least 1 because both cp at least share the main line. Invariant.Assert(baseLevelDepth >= 1); // Move the current LSCP and distance to the end of run on the base level subline. lscpCurrent = sublines[baseLevelDepth - 1].lscpFirstRun + sublines[baseLevelDepth - 1].lsdcpRun; currentDistance = GetEndOfRunDistance(sublines, baseLevelDepth - 1); } ////// Base level is the highest subline's level that both sets of sublines have in common. /// This method starts collecting text bounds at the specified LSCP. The first bounds being collected /// is the one from the LSCP to the start of run at the base level subline. The subsequent bounds are /// from the start of the higher level subline to the start of the run within the same subline, until /// it reaches the immediate enclosing subline of the last LSCP. /// private void CollectTextBoundsFromBaseLevel( ArrayList boundsList, ref int lscpCurrent, ref int currentDistance, LsQSubInfo[] sublines, int sublineDepth, int baseLevelDepth ) { // lscpCurrent is after the run end of the 1st cp. It must not be in the run of the last cp // because the two runs don't overlap at above base level. Invariant.Assert(lscpCurrent <= sublines[baseLevelDepth - 1].lscpFirstRun); // Collect the text bounds from the LSCP to the start of run at the base level subline. AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(currentDistance), 0), new LSPOINT( LSLineUToParagraphU(sublines[baseLevelDepth - 1].pointUvStartRun.x), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[baseLevelDepth - 1].lstflowSubLine), CalculateTextRunBounds(lscpCurrent, sublines[baseLevelDepth - 1].lscpFirstRun) ) ); // Collect text bounds from start of subline to start of run at higher level sublines until it // reaches the immediate enclosing subline of the last LSCP. for (int i = baseLevelDepth; i < sublineDepth - 1; i++) { AddValidTextBounds( boundsList, new TextBounds( LSRun.RectUV( new Point(0, 0), new LSPOINT(LSLineUToParagraphU(sublines[i].pointUvStartSubLine.x), 0), new LSPOINT( LSLineUToParagraphU(sublines[i].pointUvStartRun.x), _metrics._height ), _metrics._formatter.ToReal, this ), Convert.LsTFlowToFlowDirection(sublines[i].lstflowSubLine), CalculateTextRunBounds( sublines[i].lscpFirstSubLine, sublines[i].lscpFirstRun ) ) ); }; // Move the current LSCP and distance to the start of the immediate enclosing subline. lscpCurrent = sublines[sublineDepth - 1].lscpFirstSubLine; currentDistance = sublines[sublineDepth - 1].pointUvStartSubLine.x; } ////// Return the ending edge of the subline relative to its own flow direction /// private int GetEndOfSublineDistance( LsQSubInfo[] sublines, int index ) { return sublines[index].pointUvStartSubLine.x + ( sublines[index].lstflowSubLine == sublines[0].lstflowSubLine ? sublines[index].dupSubLine : -sublines[index].dupSubLine ); } ////// Return the ending edge of the run relative to its own flow direction. /// private int GetEndOfRunDistance( LsQSubInfo[] sublines, int index ) { return sublines[index].pointUvStartRun.x + ( sublines[index].lstflowSubLine == sublines[0].lstflowSubLine ? sublines[index].dupRun : -sublines[index].dupRun ); } ////// Add non-zero geometry bounds to the bounds list /// private void AddValidTextBounds( ArrayList boundsList, TextBounds bounds ) { if (bounds.Rectangle.Width != 0 && bounds.Rectangle.Height != 0) { boundsList.Add(bounds); } } ////// Compute bounds of runs within the specified range of lscp /// private IListCalculateTextRunBounds(int lscpFirst, int lscpEnd) { if (lscpEnd <= lscpFirst) { // It is possible that we'll get a legitimate case when lscpFirst is // actually greater. That's what happen when the client hittest a hidden // run that follows a reverse block. Since it is a hidden run, LS has // to yield the closest non-hidden place which may be the run preceding // the hidden text. (wchao, PS bug #930976) return null; } int lscp = lscpFirst; int cchLeft = lscpEnd - lscpFirst; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); Point position = new Point(0, 0); IList boundsList = new List (2); while(cchLeft > 0) { plsrunSpanRider.At(lscp - _cpFirst); Plsrun plsrun = (Plsrun)plsrunSpanRider.CurrentElement; int cch = Math.Min(plsrunSpanRider.Length, cchLeft); if(TextStore.IsContent(plsrun)) { LSRun lsrun = GetRun(plsrun); if( lsrun.Type == Plsrun.Text || lsrun.Type == Plsrun.InlineObject) { int cp = GetExternalCp(lscp); int cchBounds = cch; if ( HasCollapsed && _collapsedRange != null && cp <= _collapsedRange.TextSourceCharacterIndex && cp + cchBounds >= _collapsedRange.TextSourceCharacterIndex && cp + cchBounds < _collapsedRange.TextSourceCharacterIndex + _collapsedRange.Length) { // Limit the run bounds to only non-collapsed text, // we deal with collapsed text separately as it might have different flow direction. cchBounds = _collapsedRange.TextSourceCharacterIndex - cp; } if (cchBounds > 0) { TextRunBounds bounds = new TextRunBounds( LSRun.RectUV( position, new LSPOINT( LSLineUToParagraphU( DistanceFromCharacterHit(new CharacterHit(cp, 0)) ), _metrics._baselineOffset - lsrun.BaselineOffset + lsrun.BaselineMoveOffset ), new LSPOINT( LSLineUToParagraphU( DistanceFromCharacterHit(new CharacterHit(cp + cchBounds - 1, 1)) ), _metrics._baselineOffset - lsrun.BaselineOffset + lsrun.BaselineMoveOffset + lsrun.Height ), _metrics._formatter.ToReal, this ), cp, cp + cchBounds, lsrun.TextRun ); boundsList.Add(bounds); } } } cchLeft -= cch; lscp += cch; } return boundsList.Count > 0 ? boundsList : null; } /// /// Client to get a collection of TextRun objects within a line /// public override IList> GetTextRunSpans() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if (_plsrunVector == null) { // return empty textspan when the line doesn't contain text runs. return new TextSpan [0]; } IList > lsrunList = new List >(2); TextRun lastTextRun = null; int cchAcc = 0; int cchLeft = _metrics._cchLength; for (int i = 0; i < _plsrunVector.Count && cchLeft > 0; i++) { Span plsrunSpan = _plsrunVector[i]; int cch = CpCount(plsrunSpan); cch = Math.Min(cch, cchLeft); if (cch > 0) { TextRun textRun = ((LSRun)GetRun((Plsrun)plsrunSpan.element)).TextRun; Debug.Assert(textRun != null); if (lastTextRun != null && textRun != lastTextRun) { Debug.Assert(cchAcc > 0); lsrunList.Add(new TextSpan (cchAcc, lastTextRun)); cchAcc = 0; } lastTextRun = textRun; cchAcc += cch; cchLeft -= cch; } } if (lastTextRun != null) { Debug.Assert(cchAcc > 0); lsrunList.Add(new TextSpan (cchAcc, lastTextRun)); } Debug.Assert(cchLeft == 0); return lsrunList; } /// /// Client to get IndexedGlyphRuns enumerable to enumerate each IndexedGlyphRun object /// in the line. Through IndexedGlyphRun client can obtain glyph information of /// a text source character. /// ////// Critical - as this calls the Critical function UnsafeNativeMethods.LoEnumLine. /// Safe - as the IntPtr passed in is of type SecurityCriticalDataForSet which means /// it is tracked to make sure it can't be set to a random value. /// [SecurityCritical, SecurityTreatAsSafe] public override IEnumerableGetIndexedGlyphRuns() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } IEnumerable result = null; if (_ploline.Value != System.IntPtr.Zero) { TextFormatterContext context = _metrics._formatter.AcquireContext( new DrawingState(null, new Point(0, 0), null, this), _ploc.Value ); // // Kick off line enumeration // LsErr lserr = LsErr.None; LSPOINT point = new LSPOINT(0, 0); lserr = UnsafeNativeMethods.LoEnumLine( _ploline.Value, // line false, // reverse enumeration false, // geometry needed ref point // starting point ); // result result = context.IndexedGlyphRuns; // get the exception in context before it is released Exception callbackException = context.CallbackException; // clear the context context.ClearIndexedGlyphRuns(); context.Release(); if (lserr != LsErr.None) { if (callbackException != null) { // rethrow exception thrown in callbacks throw callbackException; } else { // throw with LS error codes TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.EnumLineFailure, lserr), lserr); } } } return result; } /// /// Client to acquire a state at the point where line is broken by line breaking process; /// can be null when the line ends by the ending of the paragraph. Client may pass this /// value back to TextFormatter as an input argument to TextFormatter.FormatLine when /// formatting the next line within the same paragraph. /// ////// Critical - as this calls TextMetrics.GetTextLineBreak. /// Safe - as it does not take parameter that being passed to the critical method or /// return sensitive data from the critical method. /// [SecurityCritical, SecurityTreatAsSafe] public override TextLineBreak GetTextLineBreak() { if ((_statusFlags & StatusFlags.IsDisposed) != 0) { throw new ObjectDisposedException(SR.Get(SRID.TextLineHasBeenDisposed)); } if ((_statusFlags & StatusFlags.HasCollapsed) != 0) { // collapsed line has no line break state to transfer return null; } return _metrics.GetTextLineBreak(IntPtr.Zero); } ////// Client to get the number of whitespace characters at the end of the line. /// public override int TrailingWhitespaceLength { get { // figure out number of trailing whitespaces if(_metrics._textWidth == _metrics._textWidthAtTrailing) { // LS doesnt see any trailing space (last character in a line or character before // EOP is not space). We count only the length of EOP run that we count as our // trailing whitespace. return _metrics._cchNewline; } else { // LS sees some trailing spaces (last character in a line or character before EOP // is space). We calculate number of trailing spaces based on the cp following // the last non-trailing space recognized by LS. CharacterHit characterHit = CharacterHitFromDistance(_metrics._textWidthAtTrailing + _metrics._textStart); return _cpFirst + _metrics._cchLength - characterHit.FirstCharacterIndex - characterHit.TrailingLength; } } } ////// Client to get the number of text source positions of this line /// public override int Length { get { return _metrics.Length; } } ////// Client to get the number of characters following the last character /// of the line that may trigger reformatting of the current line. /// public override int DependentLength { get { return _metrics.DependentLength; } } ////// Client to get the number of newline characters at line end /// public override int NewlineLength { get { return _metrics.NewlineLength; } } ////// Client to get distance from paragraph start to line start /// public override double Start { get { return _metrics.Start; } } ////// Client to get the total width of this line /// public override double Width { get { return _metrics.Width; } } ////// Client to get the total width of this line including width of whitespace characters at the end of the line. /// public override double WidthIncludingTrailingWhitespace { get { return _metrics.WidthIncludingTrailingWhitespace; } } ////// Client to get the height of the line /// public override double Height { get { return _metrics.Height; } } ////// Client to get the height of the text (or other content) in the line; this property may differ from the Height /// property if the client specified the line height /// public override double TextHeight { get { return _metrics.TextHeight; } } ////// Client to get the distance from top to baseline of this text line /// public override double Baseline { get { return _metrics.Baseline; } } ////// Client to get the distance from the top of the text (or other content) to the baseline of this text line; /// this property may differ from the Baseline property if the client specified the line height /// public override double TextBaseline { get { return _metrics.TextBaseline; } } ////// Client to get the distance from the before edge of line height /// to the baseline of marker of the line if any. /// public override double MarkerBaseline { get { return _metrics.MarkerBaseline; } } ////// Client to get the overall height of the list items marker of the line if any. /// public override double MarkerHeight { get { return _metrics.MarkerHeight; } } ////// Client to get the height of the actual black of the line /// public override double Extent { get { CheckBoundingBox(); return _overhang.Extent; } } ////// Client to get the distance covering all black preceding the leading edge of the line. /// public override double OverhangLeading { get { CheckBoundingBox(); return _overhang.Leading; } } ////// Client to get the distance covering all black following the trailing edge of the line. /// public override double OverhangTrailing { get { CheckBoundingBox(); return _overhang.Trailing; } } ////// Client to get the distance from the after edge of line height to the after edge of the extent of the line. /// public override double OverhangAfter { get { CheckBoundingBox(); return _overhang.Extent - Height - _overhang.Before; } } ////// Client to get a boolean value indicates whether content of the line overflows /// the specified paragraph width. /// public override bool HasOverflowed { get { return (_statusFlags & StatusFlags.HasOverflowed) != 0; } } ////// Client to get a boolean value indicates whether a line has been collapsed /// public override bool HasCollapsed { get { return (_statusFlags & StatusFlags.HasCollapsed) != 0; } } ////// Client to get a Boolean flag indicating whether the line is truncated in the /// middle of a word. This flag is set only when TextParagraphProperties.TextWrapping /// is set to TextWrapping.Wrap and a single word is longer than the formatting /// paragraph width. In such situation, TextFormatter truncates the line in the middle /// of the word to honor the desired behavior specified by TextWrapping.Wrap setting. /// public override bool IsTruncated { get { return (_statusFlags & StatusFlags.IsTruncated) != 0; } } ////// Client to get the index of the first cp of the line. /// public int CpFirst { get { return _cpFirst; } } ////// Text source of the main text for the line /// public TextSource TextSource { get { return _textSource; } } ////// Wrapper to LoQueryLinePointPcp /// ////// Critical - as this calls Critical function LoQueryLinePointPcp. /// Safe - as this doesn't take any parameters that are passed to /// LoQueryLineCpPpoint directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void QueryLinePointPcp( Point ptQuery, LsQSubInfo[] subLineInfo, out int actualDepthQuery, out LsTextCell lsTextCell ) { Debug.Assert(_ploline.Value != IntPtr.Zero); LsErr lserr = LsErr.None; lsTextCell = new LsTextCell(); unsafe { fixed(LsQSubInfo* plsqsubl = subLineInfo) { LSPOINT pt = new LSPOINT((int)ptQuery.X, (int)ptQuery.Y); lserr = UnsafeNativeMethods.LoQueryLinePointPcp( _ploline.Value, ref pt, subLineInfo.Length, (System.IntPtr)plsqsubl, out actualDepthQuery, out lsTextCell ); } } if(lserr != LsErr.None) { TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.QueryLineFailure, lserr), lserr); } if (lsTextCell.lscpEndCell < lsTextCell.lscpStartCell) { // When hit-testing is done on a generated hyphen of a hyphenated word, LS can only tell // the start LSCP and not the end LSCP. Argurably this is LS bug. In such situation they // should assume the end LSCP being the last LSCP of the line. // // However our code assumes that LS must tell both and the text cell must have size greater // than one codepoint. We count on that to reliably advance the caret position. // // The LSPTS bug#1005 has been filed and while we are still debating, we need to unblock // ourselves. What we can do is to assume that the next caret stop in this case is always // the next codepoint. lsTextCell.lscpEndCell = lsTextCell.lscpStartCell; } } ////// Wrapper to LoQueryLineCpPpoint /// ////// Critical - as this calls Critical function LoQueryLineCpPpoint. /// Safe - as this doesn't take any parameters that are passed to /// LoQueryLineCpPpoint directly for writing. /// [SecurityCritical, SecurityTreatAsSafe] private void QueryLineCpPpoint( int lscpQuery, LsQSubInfo[] subLineInfo, out int actualDepthQuery, out LsTextCell lsTextCell ) { Debug.Assert(_ploline.Value != IntPtr.Zero); LsErr lserr = LsErr.None; lsTextCell = new LsTextCell(); // Never hit LS with any LSCP beyond its last, the result is unreliable and varies between drops. int lscpValidQuery = (lscpQuery < _metrics._lscpLim ? lscpQuery : _metrics._lscpLim - 1); unsafe { fixed(LsQSubInfo* plsqsubl = subLineInfo) { lserr = UnsafeNativeMethods.LoQueryLineCpPpoint( _ploline.Value, lscpValidQuery, subLineInfo.Length, (System.IntPtr)plsqsubl, out actualDepthQuery, out lsTextCell ); } } if(lserr != LsErr.None) { TextFormatterContext.ThrowExceptionFromLsError(SR.Get(SRID.QueryLineFailure, lserr), lserr); } if (lsTextCell.lscpEndCell < lsTextCell.lscpStartCell) { // When hit-testing is done on a generated hyphen of a hyphenated word, LS can only tell // the start LSCP and not the end LSCP. Argurably this is LS bug. In such situation they // should assume the end LSCP being the last LSCP of the line. // // However our code assumes that LS must tell both and the text cell must have size greater // than one codepoint. We count on that to reliably advance the caret position. // // The LSPTS bug#1005 has been filed and while we are still debating, we need to unblock // ourselves. What we can do is to assume that the next caret stop in this case is always // the next codepoint. lsTextCell.lscpEndCell = lsTextCell.lscpStartCell; } } ////// Convert a U distance relative to start of LS line to U distance relative /// to the leading edge of paragraph. /// /// a U distance relative to start of ploline ///another U distance relative to paragraph start internal int LSLineUToParagraphU(int u) { return u + _metrics._paragraphToText - _metrics._textStart; } ////// Convert a U distance relative to the leading edge of paragraph /// to U distance relative to start of LS line. /// /// a U distance relative to paragraph start ///another U distance relative to start of ploline internal int ParagraphUToLSLineU(int u) { return u - _metrics._paragraphToText + _metrics._textStart; } internal int BaselineOffset { get { return _metrics._baselineOffset; } } internal int ParagraphWidth { get { return _paragraphWidth; } } internal double MinWidth { get { return _metrics._formatter.IdealToReal(_textMinWidthAtTrailing + _metrics._textStart); } } internal bool RightToLeft { get { return (_statusFlags & StatusFlags.RightToLeft) != 0; } } internal TextFormatterImp Formatter { get { return _metrics._formatter; } } internal TextDecorationCollection TextDecorations { get { return _paragraphTextDecorations; } } internal Brush DefaultTextDecorationsBrush { get { return _defaultTextDecorationsBrush; } } #if DEBUG internal FullTextState FullTextState { get { return _fullText; } } #endif private void BuildOverhang(Point origin, Rect boundingBox) { if(boundingBox.IsEmpty) { _overhang.Leading = _overhang.Trailing = 0; _overhang.Before = 0; _overhang.Extent = 0; } else { // Move the bounding box to the coordinate relative to the line drawing origin. // The following computation of overhang values need to be done independent to // drawing origin. boundingBox.X -= origin.X; boundingBox.Y -= origin.Y; if (RightToLeft) { double paragraphWidth = _metrics._formatter.IdealToReal(_paragraphWidth); _overhang.Leading = paragraphWidth - Start - boundingBox.Right; _overhang.Trailing = boundingBox.Left - (paragraphWidth - Start - Width); } else { _overhang.Leading = boundingBox.Left - Start; _overhang.Trailing = Start + Width - boundingBox.Right; } _overhang.Extent = boundingBox.Bottom - boundingBox.Top; _overhang.Before = -boundingBox.Top; } } ////// Overhang metrics /// private struct Overhang { internal double Leading; internal double Trailing; internal double Extent; internal double Before; } #region lsrun/cp mapping ////// Map text source CP to internal LSCP /// internal int GetInternalCp(int cp) { int lscp = _cpFirst; int cpTarget = cp; cp = lscp; foreach(Span span in _plsrunVector) { int ccp = CpCount(span); if(ccp > 0) { if(cp + ccp > cpTarget) { lscp += (ccp == span.length ? cpTarget - cp : 0); break; } cp += ccp; } lscp += span.length; } return lscp; } ////// Map internal LSCP to text source cp /// internal int GetExternalCp(int lscp) { if (lscp >= _metrics._lscpLim) { if (_collapsedRange != null) return _collapsedRange.TextSourceCharacterIndex; return _cpFirst + _metrics._cchLength; } int offsetToFirstCp; SpanRider plsrunSpanRider = new SpanRider(_plsrunVector); // skip lscp until we find one with valid map do { plsrunSpanRider.At(lscp - _cpFirst); offsetToFirstCp = GetRun(((Plsrun)plsrunSpanRider.CurrentElement)).OffsetToFirstCp; } while(offsetToFirstCp < 0 && ++lscp < _metrics._lscpLim); return offsetToFirstCp + lscp - plsrunSpanRider.CurrentSpanStart; } ////// Count actual cp of an lsrun /// /// span of plsrun ///lsrun actual cp internal int CpCount(Span plsrunSpan) { Plsrun plsrun = (Plsrun)plsrunSpan.element; plsrun = TextStore.ToIndex(plsrun); // Inline object, text or linebreak yields as many cp as what client specifies. // lsrun known only to LS e.g. reverse, yields no actual cp, if(plsrun >= Plsrun.FormatAnchor) { LSRun lsrun = GetRun(plsrun); return lsrun.Length; } return 0; } ////// Get LSRun from plsrun /// internal LSRun GetRun(Plsrun plsrun) { ArrayList lsruns = _lsrunsMainText; if (TextStore.IsMarker(plsrun)) { lsruns = _lsrunsMarkerText; } plsrun = TextStore.ToIndex(plsrun); return (LSRun)( TextStore.IsContent(plsrun) ? lsruns[(int)(plsrun - Plsrun.FormatAnchor)] : TextStore.ControlRuns[(int)plsrun] ); } #endregion } } } // File provided for Reference Use Only by Microsoft Corporation (c) 2007. // Copyright (c) Microsoft Corporation. All rights reserved.
Link Menu
This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- PackageRelationshipCollection.cs
- RTLAwareMessageBox.cs
- TagMapInfo.cs
- ContentOperations.cs
- QueueException.cs
- DBConcurrencyException.cs
- TraceRecords.cs
- PointCollectionConverter.cs
- mactripleDES.cs
- BitmapScalingModeValidation.cs
- AdapterUtil.cs
- connectionpool.cs
- SessionStateContainer.cs
- MexNamedPipeBindingCollectionElement.cs
- KeyManager.cs
- SingleAnimation.cs
- Brush.cs
- StringValidator.cs
- TextWriterEngine.cs
- ExtendedPropertyDescriptor.cs
- PipeStream.cs
- SettingsProviderCollection.cs
- FrameworkElement.cs
- shaperfactory.cs
- LogicalExpr.cs
- TextMetrics.cs
- LoadedOrUnloadedOperation.cs
- ContentElement.cs
- ControlParameter.cs
- Buffer.cs
- RelatedPropertyManager.cs
- configsystem.cs
- Events.cs
- ZoneIdentityPermission.cs
- OracleTransaction.cs
- LineVisual.cs
- SizeConverter.cs
- HttpGetProtocolImporter.cs
- IdentityReference.cs
- DockPatternIdentifiers.cs
- DbLambda.cs
- SqlFactory.cs
- HttpHostedTransportConfiguration.cs
- log.cs
- PropertyPathConverter.cs
- RotateTransform3D.cs
- Vector3DIndependentAnimationStorage.cs
- datacache.cs
- XmlChildEnumerator.cs
- SrgsText.cs
- DataGridViewRowPrePaintEventArgs.cs
- CheckoutException.cs
- PropertyDescriptorCollection.cs
- WebPartDescriptionCollection.cs
- _ConnectOverlappedAsyncResult.cs
- NonBatchDirectoryCompiler.cs
- ArraySubsetEnumerator.cs
- TaskHelper.cs
- SevenBitStream.cs
- TraceContextRecord.cs
- BrowserCapabilitiesCompiler.cs
- XmlSerializationWriter.cs
- TextServicesLoader.cs
- TableParagraph.cs
- PreparingEnlistment.cs
- FormViewModeEventArgs.cs
- SamlAssertionKeyIdentifierClause.cs
- SortExpressionBuilder.cs
- DataGridViewRow.cs
- VideoDrawing.cs
- RealizedColumnsBlock.cs
- IsolatedStorageSecurityState.cs
- PassportAuthenticationEventArgs.cs
- ParserStack.cs
- ItemChangedEventArgs.cs
- FontUnit.cs
- StyleBamlTreeBuilder.cs
- DataTable.cs
- DependencyProperty.cs
- SecurityResources.cs
- ItemAutomationPeer.cs
- GridLength.cs
- DataTransferEventArgs.cs
- SqlDataRecord.cs
- SoapAttributes.cs
- Point3D.cs
- TranslateTransform3D.cs
- AuthStoreRoleProvider.cs
- SQLByte.cs
- DefaultExpressionVisitor.cs
- WebResourceAttribute.cs
- ObjectContextServiceProvider.cs
- DeleteStoreRequest.cs
- ResXBuildProvider.cs
- FormViewInsertEventArgs.cs
- XmlILAnnotation.cs
- querybuilder.cs
- ThreadExceptionDialog.cs
- DataGridViewComboBoxColumn.cs
- FamilyTypeface.cs