/// <summary> /// Calculates the size of the specified parser token when rendered according to the current layout state. /// </summary> /// <param name="font">The current font face.</param> /// <param name="tokenType">The type of the current token.</param> /// <param name="tokenText">The text of the current token.</param> /// <param name="tokenNext">The next token after the current token, excluding command tokens.</param> /// <returns>The size of the specified token in pixels.</returns> private Size2 MeasureToken(SpriteFontFace font, TextParserTokenType tokenType, StringSegment tokenText, TextParserToken?tokenNext = null) { switch (tokenType) { case TextParserTokenType.Icon: { TextIconInfo icon; if (!registeredIcons.TryGetValue(tokenText, out icon)) { throw new InvalidOperationException(UltravioletStrings.UnrecognizedIcon.Format(tokenText)); } return(new Size2(icon.Width ?? icon.Icon.Controller.Width, icon.Height ?? icon.Icon.Controller.Height)); } case TextParserTokenType.Text: { var size = font.MeasureString(tokenText); if (tokenNext.HasValue) { var tokenNextValue = tokenNext.GetValueOrDefault(); if (tokenNextValue.TokenType == TextParserTokenType.Text && !tokenNextValue.Text.IsEmpty && !tokenNextValue.IsNewLine) { var charLast = tokenText[tokenText.Length - 1]; var charNext = tokenNextValue.Text[0]; var kerning = font.Kerning.Get(charLast, charNext); return(new Size2(size.Width + kerning, size.Height)); } } return(size); } } return(Size2.Zero); }
/// <inheritdoc/> public override SpriteFont ImportPreprocessed(ContentManager manager, IContentProcessorMetadata metadata, BinaryReader reader) { var imgDataExtension = reader.ReadString(); var imgDataLength = reader.ReadInt32(); var imgData = reader.ReadBytes(imgDataLength); Texture2D texture; using (var stream = new MemoryStream(imgData)) texture = manager.LoadFromStream <Texture2D>(stream, imgDataExtension); var glyphCount = reader.ReadInt32(); var glyphSubst = reader.ReadChar(); var glyphPositions = new Rectangle[glyphCount]; for (int i = 0; i < glyphCount; i++) { var glyphX = reader.ReadInt32(); var glyphY = reader.ReadInt32(); var glyphWidth = reader.ReadInt32(); var glyphHeight = reader.ReadInt32(); glyphPositions[i] = new Rectangle(glyphX, glyphY, glyphWidth, glyphHeight); } var fontFace = new SpriteFontFace(manager.Ultraviolet, texture, null, glyphPositions, glyphSubst, true); var font = new SpriteFont(manager.Ultraviolet, fontFace); return(font); }
/// <summary> /// Imports a font face from the specified preprocessed asset stream. /// </summary> private static SpriteFontFace ImportPreprocessedFace(ContentManager manager, IContentProcessorMetadata metadata, BinaryReader reader, IEnumerable <CharacterRegion> characterRegions) { var faceExists = reader.ReadBoolean(); if (!faceExists) { return(null); } var texturePath = reader.ReadString(); var texture = manager.Load <Texture2D>(texturePath); var textureRegionSpecified = reader.ReadBoolean(); if (textureRegionSpecified) { reader.ReadInt32(); reader.ReadInt32(); reader.ReadInt32(); reader.ReadInt32(); } var substitution = reader.ReadChar(); var glyphPositions = new List <Rectangle>(); var glyphCount = reader.ReadInt32(); for (int j = 0; j < glyphCount; j++) { var glyphX = reader.ReadInt32(); var glyphY = reader.ReadInt32(); var glyphWidth = reader.ReadInt32(); var glyphHeight = reader.ReadInt32(); glyphPositions.Add(new Rectangle(glyphX, glyphY, glyphWidth, glyphHeight)); } var face = new SpriteFontFace(manager.Ultraviolet, texture, characterRegions, glyphPositions, substitution); var kerning = new Dictionary <SpriteFontKerningPair, Int32>(); var kerningDefaultAdjustment = reader.ReadInt32(); var kerningCount = reader.ReadInt32(); for (int j = 0; j < kerningCount; j++) { var pairFirstChar = reader.ReadChar(); var pairSecondChar = reader.ReadChar(); var offset = reader.ReadInt32(); kerning[new SpriteFontKerningPair(pairFirstChar, pairSecondChar)] = offset; } face.Kerning.DefaultAdjustment = kerningDefaultAdjustment; foreach (var kvp in kerning) { face.Kerning.Set(kvp.Key, kvp.Value); } return(face); }
/// <inheritdoc/> public override SpriteFont Process(ContentManager manager, IContentProcessorMetadata metadata, SDL_Surface input) { var positions = OpenGLSpriteFontHelper.IdentifyGlyphs(input); var texture = manager.Process <SDL_Surface, Texture2D>(input); var face = new SpriteFontFace(manager.Ultraviolet, texture, null, positions, true); return(new SpriteFont(manager.Ultraviolet, face)); }
/// <summary> /// Initializes a new instance of the <see cref="TextLayoutToken"/> structure. /// </summary> /// <param name="text">The token's text.</param> /// <param name="bounds">The token's bounds relative to its layout region.</param> /// <param name="fontFace">The token's font face.</param> /// <param name="icon">The token's icon.</param> /// <param name="glyphShader">The token's glyph shader.</param> /// <param name="color">The token's color.</param> internal TextLayoutToken(StringSegment text, Rectangle bounds, SpriteFontFace fontFace, TextIconInfo? icon, GlyphShader glyphShader, Color? color) { this.text = text; this.bounds = bounds; this.fontFace = fontFace; this.icon = icon; this.glyphShader = glyphShader; this.color = color; }
/// <summary> /// Draws a string of text. /// </summary> /// <param name="fontFace">The <see cref="SpriteFontFace"/> with which to draw the text.</param> /// <param name="text">The text to draw.</param> /// <param name="position">The text's position.</param> /// <param name="color">The text's color.</param> /// <param name="rotation">The text's rotation in radians.</param> /// <param name="origin">The text's point of origin relative to its top-left corner.</param> /// <param name="scale">The text's scale factor.</param> /// <param name="effects">The text's rendering effects.</param> /// <param name="layerDepth">The text's layer depth.</param> public void DrawString(SpriteFontFace fontFace, StringSegment text, Vector2 position, Color color, Single rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, Single layerDepth) { if (SpriteBatch == null) { throw new InvalidOperationException(PresentationStrings.DrawingContextDoesNotHaveSpriteBatch); } SpriteBatch.DrawString(fontFace, text, position, color * Opacity, rotation, origin, scale, effects, layerDepth); }
/// <summary> /// Initializes a new instance of the <see cref="TextLayoutToken"/> structure. /// </summary> /// <param name="text">The token's text.</param> /// <param name="bounds">The token's bounds relative to its layout region.</param> /// <param name="fontFace">The token's font face.</param> /// <param name="icon">The token's icon.</param> /// <param name="glyphShader">The token's glyph shader.</param> /// <param name="color">The token's color.</param> internal TextLayoutToken(StringSegment text, Rectangle bounds, SpriteFontFace fontFace, TextIconInfo?icon, GlyphShader glyphShader, Color?color) { this.text = text; this.bounds = bounds; this.fontFace = fontFace; this.icon = icon; this.glyphShader = glyphShader; this.color = color; }
/// <summary> /// Draws a string of text. /// </summary> /// <param name="fontFace">The <see cref="SpriteFontFace"/> with which to draw the text.</param> /// <param name="text">The text to draw.</param> /// <param name="position">The text's position.</param> /// <param name="color">The text's color.</param> public void DrawString(SpriteFontFace fontFace, StringSegment text, Vector2 position, Color color) { if (SpriteBatch == null) { throw new InvalidOperationException(PresentationStrings.DrawingContextDoesNotHaveSpriteBatch); } SpriteBatch.DrawString(fontFace, text, position, color * Opacity); }
/// <summary> /// Processes the definition for a single font face. /// </summary> private static SpriteFontFace ProcessFace(Dictionary <String, PlatformNativeSurface> textures, ContentManager manager, IContentProcessorMetadata metadata, SpriteFontFaceDescription description, String style, IEnumerable <CharacterRegion> characterRegions) { if (description == null) { return(null); } var textureName = ResolveDependencyAssetPath(metadata, description.Texture); var textureRegion = description.TextureRegion; if (String.IsNullOrEmpty(textureName)) { throw new ContentLoadException(OpenGLStrings.InvalidSpriteFontTexture); } var faceSurface = textures[textureName]; var faceGlyphs = OpenGLSpriteFontHelper.IdentifyGlyphs(faceSurface, textureRegion); var kerningDefaultAdjustment = description.Kernings?["default"] ?? 0; var kerningPairs = description.Kernings?.Where(x => !String.Equals(x.Key, "default", StringComparison.InvariantCulture)) .ToDictionary(x => CreateKerningPair(x.Key), x => x.Value); var kerning = new SpriteFontKerning() { DefaultAdjustment = kerningDefaultAdjustment }; foreach (var kvp in kerningPairs) { kerning.Set(kvp.Key, kvp.Value); } var ascender = description.Ascender; var descender = description.Descender; var faceTexture = manager.Load <Texture2D>(textureName, metadata.AssetDensity); var face = new SpriteFontFace(manager.Ultraviolet, faceTexture, characterRegions, faceGlyphs, kerning, ascender, descender, description.Glyphs?.Substitution ?? '?'); return(face); }
/// <summary> /// Given a string and an available space, returns the largest substring which will fit within that space. /// </summary> private Boolean GetFittedSubstring(SpriteFontFace font, Int32 maxLineWidth, ref StringSegment tokenText, ref Size2 tokenSize, ref LayoutState state, Boolean hyphenate) { var substringAvailableWidth = maxLineWidth - state.PositionX; var substringWidth = 0; var substringLength = 0; for (int glyphIndex = 0; glyphIndex < tokenText.Length - 1; glyphIndex++) { var glyph1 = tokenText[glyphIndex]; var glyph2 = tokenText[glyphIndex + 1]; var glyphWidth = 0; if (hyphenate) { glyphWidth = font.MeasureGlyph(glyph1, '-').Width + font.MeasureGlyph('-').Width; } else { glyphWidth = font.MeasureGlyph(glyph1).Width; } if (substringAvailableWidth - glyphWidth < 0) { break; } var glyphSize = font.MeasureGlyph(glyph1, glyph2); substringAvailableWidth -= glyphSize.Width; substringWidth += glyphSize.Width; substringLength++; } tokenText = substringLength > 0 ? tokenText.Substring(0, substringLength) : StringSegment.Empty; tokenSize = new Size2(substringWidth, tokenSize.Height); return(substringLength > 0); }
/// <summary> /// Processes a parser token with type <see cref="TextParserTokenType.Text"/>. /// </summary> private Boolean ProcessTextToken(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace currentFontFace, ref TextParserToken token, ref LayoutState state, ref TextLayoutSettings settings, ref Int32 index) { if (token.IsNewLine) { state.AdvanceLayoutToNextLineWithBreak(output, token.SourceLength, ref settings); index++; } else { if (!AccumulateText(input, output, currentFontFace, ref index, ref state, ref settings)) return false; } return true; }
/// <summary> /// Accumulates sequential text tokens into a single text command. /// </summary> private Boolean AccumulateText(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace font, ref Int32 index, ref LayoutState state, ref TextLayoutSettings settings) { var hyphenate = (settings.Options & TextLayoutOptions.Hyphenate) == TextLayoutOptions.Hyphenate; var availableWidth = (settings.Width ?? Int32.MaxValue); var x = state.PositionX; var y = state.PositionY; var width = 0; var height = 0; var accumulatedStart = input[index].Text.Start + (state.ParserTokenOffset ?? 0); var accumulatedLength = 0; var accumulatedCount = 0; var lineOverflow = false; var lineBreakPossible = false; var tokenText = default(StringSegment); var tokenNext = default(TextParserToken?); var tokenSize = default(Size2); var tokenKerning = 0; var tokenIsBreakingSpace = false; while (index < input.Count) { var token = input[index]; if (token.TokenType != TextParserTokenType.Text || token.IsNewLine) { break; } if (!IsSegmentForCurrentSource(token.Text)) { if (accumulatedCount > 0) { break; } EmitChangeSourceIfNecessary(input, output, ref token); } tokenText = token.Text.Substring(state.ParserTokenOffset ?? 0); tokenNext = GetNextTextToken(input, index); tokenSize = MeasureToken(font, token.TokenType, tokenText, tokenNext); tokenKerning = font.Kerning.Get(tokenText[tokenText.Length - 1], ' '); // NOTE: We assume in a couple of places that tokens sizes don't exceed Int16.MaxValue, so try to // avoid accumulating tokens larger than that just in case somebody is doing something dumb if (width + tokenSize.Width > Int16.MaxValue) { break; } if (token.IsWhiteSpace && (state.LineBreakCommand == null || !token.IsNonBreakingSpace)) { lineBreakPossible = true; state.LineBreakCommand = output.Count; state.LineBreakOffset = accumulatedLength + token.Text.Length - 1; tokenIsBreakingSpace = true; } else { tokenIsBreakingSpace = false; } // For most tokens we need to bail out here if there's a line overflow, but // if it's a breaking space we need to be sure that it's part of the command stream // so that we can go back and replace it in the line break phase! var overflowsLine = state.PositionX + tokenSize.Width - tokenKerning > availableWidth; if (overflowsLine && !tokenIsBreakingSpace) { lineOverflow = true; break; } if (tokenText.Start != accumulatedStart + accumulatedLength) { break; } width = width + tokenSize.Width; height = Math.Max(height, tokenSize.Height); accumulatedLength = accumulatedLength + tokenText.Length; accumulatedCount++; state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 1, tokenText.Length); state.ParserTokenOffset = 0; state.LineLengthInCommands--; index++; // At this point, we need to bail out even for breaking spaces. if (overflowsLine && tokenIsBreakingSpace) { lineOverflow = true; break; } } if (lineBreakPossible) { var preLineBreakTextStart = accumulatedStart; var preLineBreakTextLength = state.LineBreakOffset.Value; var preLineBreakText = CreateStringSegmentFromCurrentSource(preLineBreakTextStart, preLineBreakTextLength); var preLineBreakSize = (preLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, preLineBreakText); state.BrokenTextSizeBeforeBreak = preLineBreakSize; var postLineBreakStart = accumulatedStart + (state.LineBreakOffset.Value + 1); var postLineBreakLength = accumulatedLength - (state.LineBreakOffset.Value + 1); var postLineBreakText = CreateStringSegmentFromCurrentSource(postLineBreakStart, postLineBreakLength); var postLineBreakSize = (postLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, postLineBreakText, GetNextTextToken(input, index - 1)); state.BrokenTextSizeAfterBreak = postLineBreakSize; } var bounds = new Rectangle(x, y, width, height); EmitTextIfNecessary(output, accumulatedStart, accumulatedLength, ref bounds, ref state); if (lineOverflow && !state.ReplaceLastBreakingSpaceWithLineBreak(output, ref settings)) { var overflowingToken = input[index]; if (overflowingToken.IsWhiteSpace && !overflowingToken.IsNonBreakingSpace) { output.WriteLineBreak(new TextLayoutLineBreakCommand(1)); state.AdvanceLineToNextCommand(0, 0, 1, 1, isLineBreak: true); state.AdvanceLayoutToNextLine(output, ref settings); if (overflowingToken.Text.Length > 1) { state.ParserTokenOffset = 1; } else { index++; } return(true); } if (!GetFittedSubstring(font, availableWidth, ref tokenText, ref tokenSize, ref state, hyphenate) && state.LineWidth == 0) { return(false); } var overflowingTokenBounds = (tokenText.Length == 0) ? Rectangle.Empty : new Rectangle(state.PositionX, state.PositionY, tokenSize.Width, tokenSize.Height); var overflowingTextEmitted = EmitTextIfNecessary(output, tokenText.Start, tokenText.Length, ref overflowingTokenBounds, ref state); if (overflowingTextEmitted) { state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 0, tokenText.Length); if (hyphenate) { output.WriteHyphen(); state.AdvanceLineToNextCommand(0, 0, 1, 0); } } state.ParserTokenOffset = (state.ParserTokenOffset ?? 0) + tokenText.Length; state.AdvanceLayoutToNextLine(output, ref settings); } return(true); }
/// <summary> /// Processes a parser token with type <see cref="TextParserTokenType.Text"/>. /// </summary> private Boolean ProcessTextToken(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace currentFontFace, ref TextParserToken token, ref LayoutState state, ref TextLayoutSettings settings, ref Int32 index) { if (token.IsNewLine) { state.AdvanceLayoutToNextLineWithBreak(output, token.SourceLength, ref settings); index++; } else { if (!AccumulateText(input, output, currentFontFace, ref index, ref state, ref settings)) { return(false); } } return(true); }
/// <summary> /// Gets the currently active font. /// </summary> private SpriteFont GetCurrentFont(ref TextLayoutSettings settings, Boolean bold, Boolean italic, out SpriteFontFace face) { var font = (fontStack.Count == 0) ? settings.Font : fontStack.Peek().Value; face = font.GetFace(bold, italic); return(font); }
/// <summary> /// Accumulates sequential text tokens into a single text command. /// </summary> private Boolean AccumulateText(TextParserTokenStream input, TextLayoutCommandStream output, SpriteFontFace font, ref Int32 index, ref LayoutState state, ref TextLayoutSettings settings) { var hyphenate = (settings.Options & TextLayoutOptions.Hyphenate) == TextLayoutOptions.Hyphenate; var availableWidth = (settings.Width ?? Int32.MaxValue); var x = state.PositionX; var y = state.PositionY; var width = 0; var height = 0; var accumulatedStart = input[index].Text.Start + (state.ParserTokenOffset ?? 0); var accumulatedLength = 0; var accumulatedCount = 0; var lineOverflow = false; var lineBreakPossible = false; var tokenText = default(StringSegment); var tokenNext = default(TextParserToken?); var tokenSize = default(Size2); while (index < input.Count) { var token = input[index]; if (token.TokenType != TextParserTokenType.Text || token.IsNewLine) break; if (!IsSegmentForCurrentSource(token.Text)) { if (accumulatedCount > 0) break; EmitChangeSourceIfNecessary(input, output, ref token); } tokenText = token.Text.Substring(state.ParserTokenOffset ?? 0); tokenNext = GetNextTextToken(input, index); tokenSize = MeasureToken(font, token.TokenType, tokenText, tokenNext); // NOTE: We assume in a couple of places that tokens sizes don't exceed Int16.MaxValue, so try to // avoid accumulating tokens larger than that just in case somebody is doing something dumb if (width + tokenSize.Width > Int16.MaxValue) break; var overflowsLine = state.PositionX + tokenSize.Width > availableWidth; if (overflowsLine) { lineOverflow = true; break; } if (tokenText.Start != accumulatedStart + accumulatedLength) break; if (token.IsWhiteSpace && (state.LineBreakCommand == null || !token.IsNonBreakingSpace)) { lineBreakPossible = true; state.LineBreakCommand = output.Count; state.LineBreakOffset = accumulatedLength + token.Text.Length - 1; } width = width + tokenSize.Width; height = Math.Max(height, tokenSize.Height); accumulatedLength = accumulatedLength + tokenText.Length; accumulatedCount++; state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 1, tokenText.Length); state.ParserTokenOffset = 0; state.LineLengthInCommands--; index++; } if (lineBreakPossible) { var preLineBreakTextStart = accumulatedStart; var preLineBreakTextLength = state.LineBreakOffset.Value; var preLineBreakText = CreateStringSegmentFromCurrentSource(preLineBreakTextStart, preLineBreakTextLength); var preLineBreakSize = (preLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, preLineBreakText); state.BrokenTextSizeBeforeBreak = preLineBreakSize; var postLineBreakStart = accumulatedStart + (state.LineBreakOffset.Value + 1); var postLineBreakLength = accumulatedLength - (state.LineBreakOffset.Value + 1); var postLineBreakText = CreateStringSegmentFromCurrentSource(postLineBreakStart, postLineBreakLength); var postLineBreakSize = (postLineBreakText.Length == 0) ? Size2.Zero : MeasureToken(font, TextParserTokenType.Text, postLineBreakText, GetNextTextToken(input, index - 1)); state.BrokenTextSizeAfterBreak = postLineBreakSize; } var bounds = new Rectangle(x, y, width, height); EmitTextIfNecessary(output, accumulatedStart, accumulatedLength, ref bounds, ref state); if (lineOverflow && !state.ReplaceLastBreakingSpaceWithLineBreak(output, ref settings)) { var overflowingToken = input[index]; if (overflowingToken.IsWhiteSpace && !overflowingToken.IsNonBreakingSpace) { output.WriteLineBreak(new TextLayoutLineBreakCommand(1)); state.AdvanceLineToNextCommand(0, 0, 1, 1, isLineBreak: true); state.AdvanceLayoutToNextLine(output, ref settings); if (overflowingToken.Text.Length > 1) { state.ParserTokenOffset = 1; } else { index++; } return true; } if (!GetFittedSubstring(font, availableWidth, ref tokenText, ref tokenSize, ref state, hyphenate) && state.LineWidth == 0) return false; var overflowingTokenBounds = (tokenText.Length == 0) ? Rectangle.Empty : new Rectangle(state.PositionX, state.PositionY, tokenSize.Width, tokenSize.Height); var overflowingTextEmitted = EmitTextIfNecessary(output, tokenText.Start, tokenText.Length, ref overflowingTokenBounds, ref state); if (overflowingTextEmitted) { state.AdvanceLineToNextCommand(tokenSize.Width, tokenSize.Height, 0, tokenText.Length); if (hyphenate) { output.WriteHyphen(); state.AdvanceLineToNextCommand(0, 0, 1, 0); } } state.ParserTokenOffset = (state.ParserTokenOffset ?? 0) + tokenText.Length; state.AdvanceLayoutToNextLine(output, ref settings); } return true; }
/// <summary> /// Gets the currently active font. /// </summary> private SpriteFont GetCurrentFont(ref TextLayoutSettings settings, Boolean bold, Boolean italic, out SpriteFontFace face) { var font = (fontStack.Count == 0) ? settings.Font : fontStack.Peek().Value; face = font.GetFace(bold, italic); return font; }
/// <summary> /// Calculates the size of the specified parser token when rendered according to the current layout state. /// </summary> /// <param name="font">The current font face.</param> /// <param name="tokenType">The type of the current token.</param> /// <param name="tokenText">The text of the current token.</param> /// <param name="tokenNext">The next token after the current token, excluding command tokens.</param> /// <returns>The size of the specified token in pixels.</returns> private Size2 MeasureToken(SpriteFontFace font, TextParserTokenType tokenType, StringSegment tokenText, TextParserToken? tokenNext = null) { switch (tokenType) { case TextParserTokenType.Icon: { TextIconInfo icon; if (!registeredIcons.TryGetValue(tokenText, out icon)) throw new InvalidOperationException(UltravioletStrings.UnrecognizedIcon.Format(tokenText)); return new Size2(icon.Width ?? icon.Icon.Controller.Width, icon.Height ?? icon.Icon.Controller.Height); } case TextParserTokenType.Text: { var size = font.MeasureString(tokenText); if (tokenNext.HasValue) { var tokenNextValue = tokenNext.GetValueOrDefault(); if (tokenNextValue.TokenType == TextParserTokenType.Text && !tokenNextValue.Text.IsEmpty && !tokenNextValue.IsNewLine) { var charLast = tokenText[tokenText.Length - 1]; var charNext = tokenNextValue.Text[0]; var kerning = font.Kerning.Get(charLast, charNext); return new Size2(size.Width + kerning, size.Height); } } return size; } } return Size2.Zero; }
/// <summary> /// Given a string and an available space, returns the largest substring which will fit within that space. /// </summary> private Boolean GetFittedSubstring(SpriteFontFace font, Int32 maxLineWidth, ref StringSegment tokenText, ref Size2 tokenSize, ref LayoutState state, Boolean hyphenate) { var substringAvailableWidth = maxLineWidth - state.PositionX; var substringWidth = 0; var substringLength = 0; for (int glyphIndex = 0; glyphIndex < tokenText.Length - 1; glyphIndex++) { var glyph1 = tokenText[glyphIndex]; var glyph2 = tokenText[glyphIndex + 1]; var glyphWidth = 0; if (hyphenate) { glyphWidth = font.MeasureGlyph(glyph1, '-').Width + font.MeasureGlyph('-').Width; } else { glyphWidth = font.MeasureGlyph(glyph1).Width; } if (substringAvailableWidth - glyphWidth < 0) break; var glyphSize = font.MeasureGlyph(glyph1, glyph2); substringAvailableWidth -= glyphSize.Width; substringWidth += glyphSize.Width; substringLength++; } tokenText = substringLength > 0 ? tokenText.Substring(0, substringLength) : StringSegment.Empty; tokenSize = new Size2(substringWidth, tokenSize.Height); return substringLength > 0; }