From 90e0ff55d13bfd2ba4a3426c5f49810da78ea09f Mon Sep 17 00:00:00 2001 From: Ellpeck Date: Sat, 20 Jun 2020 01:18:27 +0200 Subject: [PATCH] use a custom character for image codes rather than trying to calculate spaces --- MLEM.Extended/Font/GenericBitmapFont.cs | 34 ++----------- MLEM.Ui/Elements/TextField.cs | 2 +- MLEM.Ui/Style/UntexturedStyle.cs | 12 +---- MLEM/Font/GenericFont.cs | 63 +++++++++++++++---------- MLEM/Font/GenericSpriteFont.cs | 25 +++++----- MLEM/Formatting/Codes/ImageCode.cs | 13 +---- Sandbox/Content/Fonts/Regular.fnt | 2 +- Sandbox/GameImpl.cs | 17 ++++--- 8 files changed, 68 insertions(+), 100 deletions(-) diff --git a/MLEM.Extended/Font/GenericBitmapFont.cs b/MLEM.Extended/Font/GenericBitmapFont.cs index 7cf14ec..9f3dc1d 100644 --- a/MLEM.Extended/Font/GenericBitmapFont.cs +++ b/MLEM.Extended/Font/GenericBitmapFont.cs @@ -32,18 +32,10 @@ namespace MLEM.Extended.Font { this.Italic = italic != null ? new GenericBitmapFont(italic) : this; } - /// - public override Vector2 MeasureString(string text) { - if (text.Length == 1 && this.SingleCharacterWidthFix(text, out var size)) - return size; - return this.Font.MeasureString(text); - } - - /// - public override Vector2 MeasureString(StringBuilder text) { - if (text.Length == 1 && this.SingleCharacterWidthFix(text.ToString(), out var size)) - return size; - return this.Font.MeasureString(text); + /// + protected override Vector2 CalcCharSize(char c) { + var region = this.Font.GetCharacterRegion(c); + return region != null ? new Vector2(region.XAdvance, region.Height) : Vector2.Zero; } /// @@ -76,23 +68,5 @@ namespace MLEM.Extended.Font { batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth); } - /// - public override bool HasCharacter(char c) { - return this.Font.GetCharacterRegion(c) != null; - } - - // this fixes an issue with BitmapFonts where, if only given a single character, - // only the width of the character itself (disregarding spacing) is returned - private bool SingleCharacterWidthFix(string text, out Vector2 size) { - var codePoint = char.ConvertToUtf32(text, 0); - var region = this.Font.GetCharacterRegion(codePoint); - if (region != null) { - size = new Vector2(region.XAdvance, region.Height); - return true; - } - size = default; - return false; - } - } } \ No newline at end of file diff --git a/MLEM.Ui/Elements/TextField.cs b/MLEM.Ui/Elements/TextField.cs index 4a0fd57..1330fdb 100644 --- a/MLEM.Ui/Elements/TextField.cs +++ b/MLEM.Ui/Elements/TextField.cs @@ -164,7 +164,7 @@ namespace MLEM.Ui.Elements { // not initialized yet if (!this.Font.HasValue()) return; - var length = this.Font.Value.MeasureString(this.text).X * this.TextScale; + var length = this.Font.Value.MeasureString(this.text.ToString()).X * this.TextScale; var maxWidth = this.DisplayArea.Width / this.Scale - this.TextOffsetX * 2; if (length > maxWidth) { // if we're moving the caret to the left diff --git a/MLEM.Ui/Style/UntexturedStyle.cs b/MLEM.Ui/Style/UntexturedStyle.cs index bdb80d9..b0a7b72 100644 --- a/MLEM.Ui/Style/UntexturedStyle.cs +++ b/MLEM.Ui/Style/UntexturedStyle.cs @@ -46,12 +46,8 @@ namespace MLEM.Ui.Style { public override GenericFont Italic => this; public override float LineHeight => 1; - public override Vector2 MeasureString(string text) { - return Vector2.One; - } - - public override Vector2 MeasureString(StringBuilder text) { - return Vector2.One; + protected override Vector2 CalcCharSize(char c) { + return Vector2.Zero; } public override void DrawString(SpriteBatch batch, string text, Vector2 position, Color color) { @@ -72,10 +68,6 @@ namespace MLEM.Ui.Style { public override void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth) { } - public override bool HasCharacter(char c) { - return false; - } - } } diff --git a/MLEM/Font/GenericFont.cs b/MLEM/Font/GenericFont.cs index 8e51730..148fd5d 100644 --- a/MLEM/Font/GenericFont.cs +++ b/MLEM/Font/GenericFont.cs @@ -1,6 +1,7 @@ using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; +using MLEM.Formatting.Codes; namespace MLEM.Font { /// @@ -9,6 +10,13 @@ namespace MLEM.Font { /// public abstract class GenericFont { + /// + /// This field holds a special, private use area code point for a one em space. + /// This is a character that isn't drawn, but has the same width as . + /// It is mainly used for . + /// + public const char OneEmSpace = '\uF8FF'; + /// /// The bold version of this font. /// @@ -22,12 +30,6 @@ namespace MLEM.Font { /// public abstract float LineHeight { get; } - /// - public abstract Vector2 MeasureString(string text); - - /// - public abstract Vector2 MeasureString(StringBuilder text); - /// public abstract void DrawString(SpriteBatch batch, string text, Vector2 position, Color color); @@ -46,12 +48,8 @@ namespace MLEM.Font { /// public abstract void DrawString(SpriteBatch batch, StringBuilder text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth); - /// - /// Returns whether this generic font supports the given character - /// - /// The character - /// Whether this generic font supports the character - public abstract bool HasCharacter(char c); + /// + protected abstract Vector2 CalcCharSize(char c); /// /// Draws a string with the given text alignment. @@ -87,6 +85,32 @@ namespace MLEM.Font { this.DrawString(batch, text, position, color, rotation, origin, scale, effects, layerDepth); } + /// + public Vector2 MeasureChar(char c) { + return c == OneEmSpace ? new Vector2(this.LineHeight) : this.CalcCharSize(c); + } + + /// + public Vector2 MeasureString(string text) { + var size = Vector2.Zero; + var xOffset = 0F; + foreach (var c in text) { + if (c == '\n') { + xOffset = 0; + size.Y += this.LineHeight; + continue; + } + + xOffset += this.MeasureChar(c).X; + // increase x size if this line is the longest + if (xOffset > size.X) + size.X = xOffset; + } + // include the last line's height too! + size.Y += this.LineHeight; + return size; + } + /// /// Truncates a string to a given width. If the string's displayed area is larger than the maximum width, the string is cut off. /// Optionally, the string can be cut off a bit sooner, adding the at the end instead. @@ -107,7 +131,7 @@ namespace MLEM.Font { total.Append(text[i]); } - if (this.MeasureString(total).X * scale + ellipsisWidth >= width) { + if (this.MeasureString(total.ToString()).X * scale + ellipsisWidth >= width) { if (fromBack) { return total.Remove(0, 1).Insert(0, ellipsis).ToString(); } else { @@ -161,19 +185,6 @@ namespace MLEM.Font { return total.ToString(0, total.Length - 2); } - /// - /// Returns a string made up of the given content characters that is the given length long when displayed. - /// - /// The width that the string should have if the scale is 1 - /// The content that the string should contain. Defaults to a space. - /// - public string GetWidthString(float width, char content = ' ') { - var strg = content.ToString(); - while (this.MeasureString(strg).X < width) - strg += content; - return strg; - } - } /// diff --git a/MLEM/Font/GenericSpriteFont.cs b/MLEM/Font/GenericSpriteFont.cs index d25afe1..feb8916 100644 --- a/MLEM/Font/GenericSpriteFont.cs +++ b/MLEM/Font/GenericSpriteFont.cs @@ -25,19 +25,14 @@ namespace MLEM.Font { /// A bold version of the font /// An italic version of the font public GenericSpriteFont(SpriteFont font, SpriteFont bold = null, SpriteFont italic = null) { - this.Font = font; - this.Bold = bold != null ? new GenericSpriteFont(bold) : this; - this.Italic = italic != null ? new GenericSpriteFont(italic) : this; + this.Font = SetDefaults(font); + this.Bold = bold != null ? new GenericSpriteFont(SetDefaults(bold)) : this; + this.Italic = italic != null ? new GenericSpriteFont(SetDefaults(italic)) : this; } - /// - public override Vector2 MeasureString(string text) { - return this.Font.MeasureString(text); - } - - /// - public override Vector2 MeasureString(StringBuilder text) { - return this.Font.MeasureString(text); + /// + protected override Vector2 CalcCharSize(char c) { + return this.Font.MeasureString(c.ToString()); } /// @@ -70,9 +65,11 @@ namespace MLEM.Font { batch.DrawString(this.Font, text, position, color, rotation, origin, scale, effects, layerDepth); } - /// - public override bool HasCharacter(char c) { - return this.Font.Characters.Contains(c); + private static SpriteFont SetDefaults(SpriteFont font) { + // so that missing character behavior is in line with MG.Extended's + // bitmap fonts, we draw nothing as the default character + font.DefaultCharacter = ' '; + return font; } } diff --git a/MLEM/Formatting/Codes/ImageCode.cs b/MLEM/Formatting/Codes/ImageCode.cs index c6a97cb..5951c17 100644 --- a/MLEM/Formatting/Codes/ImageCode.cs +++ b/MLEM/Formatting/Codes/ImageCode.cs @@ -12,8 +12,6 @@ namespace MLEM.Formatting.Codes { public class ImageCode : Code { private readonly SpriteAnimation image; - private string replacement; - private float gapSize; /// public ImageCode(Match match, Regex regex, SpriteAnimation image) : base(match, regex) { @@ -27,13 +25,7 @@ namespace MLEM.Formatting.Codes { /// public override string GetReplacementString(GenericFont font) { - if (this.replacement == null) { - // use non-breaking space so that the image won't be line-splitted - var strg = font.GetWidthString(font.LineHeight, font.HasCharacter('\u00A0') ? '\u00A0' : ' '); - this.replacement = strg.Remove(strg.Length - 1) + ' '; - this.gapSize = font.MeasureString(this.replacement).X; - } - return this.replacement; + return GenericFont.OneEmSpace.ToString(); } /// @@ -43,8 +35,7 @@ namespace MLEM.Formatting.Codes { /// public override void DrawSelf(GameTime time, SpriteBatch batch, Vector2 pos, GenericFont font, Color color, float scale, float depth) { - var position = pos + new Vector2(this.gapSize - font.LineHeight, 0) / 2 * scale; - batch.Draw(this.image.CurrentRegion, new RectangleF(position, new Vector2(font.LineHeight * scale)), Color.White.CopyAlpha(color)); + batch.Draw(this.image.CurrentRegion, new RectangleF(pos, new Vector2(font.LineHeight * scale)), Color.White.CopyAlpha(color)); } } diff --git a/Sandbox/Content/Fonts/Regular.fnt b/Sandbox/Content/Fonts/Regular.fnt index bee2d8c..dd46e16 100644 --- a/Sandbox/Content/Fonts/Regular.fnt +++ b/Sandbox/Content/Fonts/Regular.fnt @@ -1,7 +1,7 @@ - + diff --git a/Sandbox/GameImpl.cs b/Sandbox/GameImpl.cs index c3b2d47..d60faa9 100644 --- a/Sandbox/GameImpl.cs +++ b/Sandbox/GameImpl.cs @@ -10,6 +10,7 @@ using MLEM.Extended.Extensions; using MLEM.Extended.Font; using MLEM.Extended.Tiled; using MLEM.Extensions; +using MLEM.Font; using MLEM.Formatting; using MLEM.Formatting.Codes; using MLEM.Input; @@ -19,6 +20,7 @@ using MLEM.Textures; using MLEM.Ui; using MLEM.Ui.Elements; using MLEM.Ui.Style; +using MonoGame.Extended; using MonoGame.Extended.BitmapFonts; using MonoGame.Extended.Tiled; using RectangleF = MonoGame.Extended.RectangleF; @@ -118,27 +120,28 @@ namespace Sandbox { this.SpriteBatch.End(); };*/ + var sc = 4; var formatter = new TextFormatter(); formatter.AddImage("Test", new TextureRegion(tex, 0, 8, 24, 24)); - var strg = "This is a formatted string with two bits of formatting! It also includesaverylongwordthatisformattedaswell. Additionally, it wobbles and has a shadow or a weird shadow. We like icons too! "; + var strg = "Additionally, it wobbles and has a shadow or a weird shadow. We like icons too! "; //var strg = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; //var strg = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; //var strg = "This is a test of the underlined formatting code!"; this.tokenized = formatter.Tokenize(font, strg); - this.tokenized.Split(font, 400, 5); + this.tokenized.Split(font, 400, sc); - /*this.OnDraw += (g, time) => { + this.OnDraw += (g, time) => { this.SpriteBatch.Begin(samplerState: SamplerState.PointClamp); this.SpriteBatch.FillRectangle(new RectangleF(400, 20, 400, 1000), Color.Green); - font.DrawString(this.SpriteBatch, this.tokenized.DisplayString, new Vector2(400, 20), Color.White * 0.25F, 0, Vector2.Zero, 5, SpriteEffects.None, 0); - this.tokenized.Draw(time, this.SpriteBatch, new Vector2(400, 20), font, Color.White, 5, 0); + font.DrawString(this.SpriteBatch, this.tokenized.DisplayString, new Vector2(400, 20), Color.White * 0.25F, 0, Vector2.Zero, sc, SpriteEffects.None, 0); + this.tokenized.Draw(time, this.SpriteBatch, new Vector2(400, 20), font, Color.White, sc, 0); this.SpriteBatch.DrawGrid(new Vector2(30, 30), new Vector2(40, 60), new Point(10, 5), Color.Yellow, 3); this.SpriteBatch.End(); - };*/ + }; this.OnUpdate += (g, time) => { if (this.InputHandler.IsPressed(Keys.W)) { this.tokenized = formatter.Tokenize(font, strg); - this.tokenized.Split(font, this.InputHandler.IsModifierKeyDown(ModifierKey.Shift) ? 400 : 500, 5); + this.tokenized.Split(font, this.InputHandler.IsModifierKeyDown(ModifierKey.Shift) ? 400 : 500, sc); } this.tokenized.Update(time); };