/// <summary> /// Cache index to the list of scaled shapeable typeface /// </summary> private void CacheScaledTypefaceMap( CharacterBufferRange unicodeString, CultureInfo culture, CultureInfo digitCulture, SpanVector scaledTypefaceSpans, ref SpanVector<int> cachedScaledTypefaceIndexSpans, int ichItem ) { IntMap map; if (!_intMaps.TryGetValue(culture, out map)) { map = new IntMap(); _intMaps.Add(culture, map); } DigitMap digitMap = new DigitMap(digitCulture); SpanRider typefaceSpanRider = new SpanRider(scaledTypefaceSpans); int ich = 0; while(ich < unicodeString.Length) { typefaceSpanRider.At(ich); int cch = Math.Min(unicodeString.Length - ich, typefaceSpanRider.Length); int index = IndexOfScaledTypeface((ScaledShapeTypeface)typefaceSpanRider.CurrentElement); Debug.Assert(index >= 0, "Invalid scaled shapeable typeface index spans"); cachedScaledTypefaceIndexSpans.Set(ichItem + ich, cch, index); // we keep index + 1 in the map, so that we leave map entry zero // to indicate uninitialized entry. index++; int sizeofChar; for (int c = 0; c < cch; c += sizeofChar) { int ch = digitMap[ Classification.UnicodeScalar( new CharacterBufferRange(unicodeString, ich + c, unicodeString.Length - ich - c), out sizeofChar ) ]; // only cache typeface map index for base characters if(!Classification.IsCombining(ch) && !Classification.IsJoiner(ch)) { // Dump values of local variables when the condition fails for better debuggability. // We use "if" to avoid the expensive string.Format() in normal case. if (map[ch] != 0 && map[ch] != index) { Invariant.Assert( false, string.Format( CultureInfo.InvariantCulture, "shapeable cache stores conflicting info, ch = {0}, map[ch] = {1}, index = {2}", ch, map[ch], index ) ); } map[ch] = (ushort)index; } } ich += cch; } }
internal void GetShapeableText( CharacterBufferReference characterBufferReference, int stringLength, TextRunProperties textRunProperties, CultureInfo digitCulture, bool isRightToLeftParagraph, IList<TextShapeableSymbols> shapeableList, IShapeableTextCollector collector, TextFormattingMode textFormattingMode ) { SpanVector<int> cachedScaledTypefaceIndexSpans; int ichItem = 0; CharacterBufferRange unicodeString = new CharacterBufferRange( characterBufferReference, stringLength ); CultureInfo culture = textRunProperties.CultureInfo; IList<Span> spans; GCHandle gcHandle; IntPtr ptext = characterBufferReference.CharacterBuffer.PinAndGetCharacterPointer(characterBufferReference.OffsetToFirstChar, out gcHandle); // Contextual number substitution cannot be performed on the run level, since it depends // on context - nearest preceding strong character. For this reason, contextual number // substitutions has been already done (TextStore.CreateLSRunsUniformBidiLevel) and // digitCulture has been updated to reflect culture which is dependent on the context. // NumberSubstitutionMethod.AsCulture method can be resolved to Context, hence it also needs to be resolved to appropriate // not ambiguous method. // Both of those values (Context and AsCulture) are resolved to one of following: European, Traditional or NativeNational, // which can be safely handled by DWrite without getting context information. bool ignoreUserOverride; NumberSubstitutionMethod numberSubstitutionMethod = DigitState.GetResolvedSubstitutionMethod(textRunProperties, digitCulture, out ignoreUserOverride); // Itemize the text based on DWrite's text analysis for scripts and number substitution. unsafe { checked { spans = MS.Internal.Text.TextInterface.TextAnalyzer.Itemize( (ushort*)ptext.ToPointer(), (uint)stringLength, culture, MS.Internal.FontCache.DWriteFactory.Instance, isRightToLeftParagraph, digitCulture, ignoreUserOverride, (uint)numberSubstitutionMethod, ClassificationUtility.Instance, UnsafeNativeMethods.CreateTextAnalysisSink, UnsafeNativeMethods.GetScriptAnalysisList, UnsafeNativeMethods.GetNumberSubstitutionList, UnsafeNativeMethods.CreateTextAnalysisSource ); } } characterBufferReference.CharacterBuffer.UnpinCharacterPointer(gcHandle); SpanVector itemSpans = new SpanVector(null, new FrugalStructList<Span>((ICollection<Span>)spans)); cachedScaledTypefaceIndexSpans = new SpanVector<int>(-1); foreach(Span itemSpan in itemSpans) { MapItem( new CharacterBufferRange( unicodeString, ichItem, itemSpan.length ), culture, itemSpan, ref cachedScaledTypefaceIndexSpans, ichItem ); #if DEBUG ValidateMapResult( ichItem, itemSpan.length, ref cachedScaledTypefaceIndexSpans ); #endif ichItem += itemSpan.length; } Debug.Assert(ichItem == unicodeString.Length); // intersect item spans with shapeable spans to create span of shapeable runs int ich = 0; SpanRider itemSpanRider = new SpanRider(itemSpans); SpanRider<int> typefaceIndexSpanRider = new SpanRider<int>(cachedScaledTypefaceIndexSpans); while(ich < unicodeString.Length) { itemSpanRider.At(ich); typefaceIndexSpanRider.At(ich); int index = typefaceIndexSpanRider.CurrentValue; Debug.Assert(index >= 0); int cch = unicodeString.Length - ich; cch = Math.Min(cch, itemSpanRider.Length); cch = Math.Min(cch, typefaceIndexSpanRider.Length); ScaledShapeTypeface scaledShapeTypeface = _cachedScaledTypefaces[index]; collector.Add( shapeableList, new CharacterBufferRange( unicodeString, ich, cch ), textRunProperties, (MS.Internal.Text.TextInterface.ItemProps)itemSpanRider.CurrentElement, scaledShapeTypeface.ShapeTypeface, scaledShapeTypeface.ScaleInEm, scaledShapeTypeface.NullShape, textFormattingMode ); ich += cch; } }
/// <summary> /// Map internal LSCP to text source cp /// </summary> 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; }
private unsafe void ValidateMapResult( int ichRange, int cchRange, ref SpanVector<int> cachedScaledTypefaceIndexSpans ) { int ich = 0; SpanRider<int> typefaceIndexSpanRider = new SpanRider<int>(cachedScaledTypefaceIndexSpans); while(ich < cchRange) { typefaceIndexSpanRider.At(ichRange + ich); if((int)typefaceIndexSpanRider.CurrentValue < 0) { Debug.Assert(false, "Invalid font face spans"); return; } int cch = Math.Min(cchRange - ich, typefaceIndexSpanRider.Length); ich += cch; } }
/// <summary> /// 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. /// </summary> 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; }
/// <summary> /// Compute bounds of runs within the specified range of lscp /// </summary> private IList<TextRunBounds> CalculateTextRunBounds(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<TextRunBounds> boundsList = new List<TextRunBounds>(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 ), this ), cp, cp + cchBounds, lsrun.TextRun ); boundsList.Add(bounds); } } } cchLeft -= cch; lscp += cch; } return boundsList.Count > 0 ? boundsList : null; }
/// <summary> /// Sets or changes the text decorations /// </summary> /// <param name="textDecorations">Text decorations</param> /// <param name="startIndex">The start index of initial character to apply the change to.</param> /// <param name="count">The number of characters the change should be applied to.</param> public void SetTextDecorations(TextDecorationCollection textDecorations, int startIndex, int count) { int limit = ValidateRange(startIndex, count); for (int i = startIndex; i < limit;) { SpanRider formatRider = new SpanRider(_formatRuns, _latestPosition, i); i = Math.Min(limit, i + formatRider.Length); #pragma warning disable 6506 // Presharp warns that runProps is not validated, but it can never be null // because the rider is already checked to be in range GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties; Invariant.Assert(runProps != null); if (runProps.TextDecorations == textDecorations) continue; GenericTextRunProperties newProps = new GenericTextRunProperties( runProps.Typeface, runProps.FontRenderingEmSize, runProps.FontHintingEmSize, textDecorations, runProps.ForegroundBrush, runProps.BackgroundBrush, runProps.BaselineAlignment, runProps.CultureInfo, runProps.NumberSubstitution ); #pragma warning restore 6506 _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition); } }
/// <summary> /// Sets or changes the font style /// </summary> /// <param name="style">Font style</param> /// <param name="startIndex">The start index of initial character to apply the change to.</param> /// <param name="count">The number of characters the change should be applied to.</param> public void SetFontStyle(FontStyle style, int startIndex, int count) { int limit = ValidateRange(startIndex, count); for (int i = startIndex; i < limit;) { SpanRider formatRider = new SpanRider(_formatRuns, _latestPosition, i); i = Math.Min(limit, i + formatRider.Length); #pragma warning disable 6506 // Presharp warns that runProps is not validated, but it can never be null // because the rider is already checked to be in range GenericTextRunProperties runProps = formatRider.CurrentElement as GenericTextRunProperties; Invariant.Assert(runProps != null); Typeface oldTypeface = runProps.Typeface; if (oldTypeface.Style == style) continue; GenericTextRunProperties newProps = new GenericTextRunProperties( new Typeface(oldTypeface.FontFamily, style, oldTypeface.Weight, oldTypeface.Stretch), runProps.FontRenderingEmSize, runProps.FontHintingEmSize, runProps.TextDecorations, runProps.ForegroundBrush, runProps.BackgroundBrush, runProps.BaselineAlignment, runProps.CultureInfo, runProps.NumberSubstitution ); #pragma warning restore 6506 _latestPosition = _formatRuns.SetValue(formatRider.CurrentPosition, i - formatRider.CurrentPosition, newProps, formatRider.SpanPosition); InvalidateMetrics(); // invalidate cached metrics } }
/// <summary> /// TextFormatter to get text immediately before specified text source position. /// </summary> /// <param name="textSourceCharacterIndexLimit">character index to specify where in the source text the text retrieval stops.</param> /// <returns>character string immediately before the specify text source character index.</returns> public override TextSpan<CultureSpecificCharacterBufferRange> GetPrecedingText( int textSourceCharacterIndexLimit ) { CharacterBufferRange charString = CharacterBufferRange.Empty; CultureInfo culture = null; if (textSourceCharacterIndexLimit > 0) { SpanRider thatFormatRider = new SpanRider( _that._formatRuns, _that._latestPosition, textSourceCharacterIndexLimit - 1 ); charString = new CharacterBufferRange( new CharacterBufferReference(_that._text, thatFormatRider.CurrentSpanStart), textSourceCharacterIndexLimit - thatFormatRider.CurrentSpanStart ); culture = ((TextRunProperties)thatFormatRider.CurrentElement).CultureInfo; } return new TextSpan<CultureSpecificCharacterBufferRange> ( charString.Length, new CultureSpecificCharacterBufferRange(culture, charString) ); }
/// <summary> /// TextFormatter to get a text run started at specified text source position /// </summary> /// <param name="textSourceCharacterIndex">character index to specify where in the source text the fetch is to start.</param> public override TextRun GetTextRun( int textSourceCharacterIndex ) { if (textSourceCharacterIndex >= _that._text.Length) { return new TextEndOfParagraph(1); } SpanRider thatFormatRider = new SpanRider( _that._formatRuns, _that._latestPosition, textSourceCharacterIndex ); return new TextCharacters(_that._text, textSourceCharacterIndex, thatFormatRider.Length, thatFormatRider.CurrentElement as GenericTextRunProperties ); }
/// <summary> /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// </summary> private TextLine FormatLine(TextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak lineBreak) { TextLine line = _formatter.FormatLine( textSource, textSourcePosition, maxLineLength, paraProps, lineBreak ); if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) { // what I really need here is the last displayed text run of the line // textSourcePosition + line.Length - 1 works except the end of paragraph case, // where line length includes the fake paragraph break run Debug.Assert(_that._text.Length > 0 && textSourcePosition + line.Length <= _that._text.Length + 1); SpanRider thatFormatRider = new SpanRider( _that._formatRuns, _that._latestPosition, Math.Min(textSourcePosition + line.Length - 1, _that._text.Length - 1) ); GenericTextRunProperties lastRunProps = thatFormatRider.CurrentElement as GenericTextRunProperties; TextCollapsingProperties trailingEllipsis; if (_that._trimming == TextTrimming.CharacterEllipsis) trailingEllipsis = new TextTrailingCharacterEllipsis(maxLineLength, lastRunProps); else { Debug.Assert(_that._trimming == TextTrimming.WordEllipsis); trailingEllipsis = new TextTrailingWordEllipsis(maxLineLength, lastRunProps); } TextLine collapsedLine = line.Collapse(trailingEllipsis); if (collapsedLine != line) { line.Dispose(); line = collapsedLine; } } return line; }