using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MLEM.Font;
using MLEM.Formatting;
using MLEM.Formatting.Codes;
using MLEM.Graphics;
using MLEM.Maths;
using MLEM.Startup;
using MLEM.Textures;
namespace Demos {
public class TextFormattingDemo : Demo {
private const string Text =
"MLEM's text formatting system allows for various formatting codes to be applied in the middle of a string. Here's a demonstration of some of them.\n\n" +
"You can write in bold, italics, with an underline, strikethrough, with a drop shadow whose color and offset you can modify in each application of the code, with an outline that you can also modify dynamically, or with various types of combined formatting codes.\n\n" +
"You can apply custom colors to text, including all default MonoGame colors and inline custom colors.\n\n" +
"You can also use animations like a wobbly one, as well as create custom ones using the Code class.\n\n" +
"You can also display icons in your text, and use superscript or subscript formatting!\n\n" +
"Additionally, the text formatter has various methods for interacting with the text, like custom behaviors when hovering over certain parts, and more.";
private const float DefaultScale = 0.5F;
private const float WidthMultiplier = 0.9F;
private TextFormatter formatter;
private TokenizedString tokenizedText;
private GenericFont font;
private bool drawBounds;
private bool transform;
private float Scale {
get {
// calculate our scale based on how much larger the window is, so that the text scales with the window
var viewport = new Rectangle(0, 0, this.Game.Window.ClientBounds.Width, this.Game.Window.ClientBounds.Height);
return TextFormattingDemo.DefaultScale * Math.Min(viewport.Width / 1280F, viewport.Height / 720F);
}
}
private int startIndex;
private int endIndex;
public TextFormattingDemo(MlemGame game) : base(game) {}
public override void LoadContent() {
this.Game.Window.ClientSizeChanged += this.OnResize;
// creating a new text formatter as well as a generic font to draw with
this.formatter = new TextFormatter {
DefaultShadowOffset = new Vector2(4),
DefaultOutlineThickness = 4
};
// GenericFont and its subtypes are wrappers around various font classes, including SpriteFont, MonoGame.Extended's BitmapFont and FontStashSharp
// supplying a bold and italic version of the font here allows for the bold and italic formatting codes to be used
this.font = new GenericSpriteFont(
Demo.LoadContent("Fonts/Roboto"),
Demo.LoadContent("Fonts/RobotoBold"),
Demo.LoadContent("Fonts/RobotoItalic"));
// adding the image code used in the example to it
var testTexture = Demo.LoadContent("Textures/Test");
this.formatter.AddImage("grass", new TextureRegion(testTexture, 0, 0, 8, 8));
// tokenizing our text and splitting it to fit the screen
// we specify our text alignment here too, so that all data is cached correctly for display
this.tokenizedText = this.formatter.Tokenize(this.font, TextFormattingDemo.Text, TextAlignment.Center);
this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.WidthMultiplier, this.Scale, TextAlignment.Center);
this.endIndex = this.tokenizedText.String.Length;
}
public override void DoDraw(GameTime time) {
this.GraphicsDevice.Clear(Color.DarkSlateGray);
this.SpriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullCounterClockwise);
// we draw the tokenized text in the center of the screen
// since the text is already center-aligned, we only need to align it on the y axis here
var size = this.tokenizedText.GetArea(scale: this.Scale).Size;
var pos = new Vector2(this.GraphicsDevice.Viewport.Width / 2, (this.GraphicsDevice.Viewport.Height - size.Y) / 2);
var rotation = this.transform ? 0.25F : 0;
var origin = this.transform ? new Vector2(size.X / this.Scale, 0) : Vector2.Zero;
var effects = this.transform ? SpriteEffects.FlipHorizontally : SpriteEffects.None;
// draw bounds, which can be toggled with B in this demo
if (this.drawBounds) {
var blank = this.SpriteBatch.GetBlankTexture();
this.SpriteBatch.Draw(blank, new RectangleF(pos - new Vector2(size.X / 2, 0), size), Color.Red * 0.25F);
foreach (var token in this.tokenizedText.Tokens) {
foreach (var area in token.GetArea(pos, this.Scale))
this.SpriteBatch.Draw(blank, area, Color.Black * 0.25F);
}
}
// draw the text itself (start and end indices are optional)
this.tokenizedText.Draw(time, this.SpriteBatch, pos, this.font, Color.White, this.Scale, 0, rotation, origin, effects, this.startIndex, this.endIndex);
this.SpriteBatch.End();
// an example of how to interact with the text
var hovered = this.tokenizedText.GetTokenUnderPos(pos, this.InputHandler.ViewportMousePosition.ToVector2(), this.Scale, this.font, rotation, origin, effects);
if (hovered != null)
Console.WriteLine($"Hovering \"{hovered.Substring}\"");
}
public override void Update(GameTime time) {
// update our tokenized string to animate the animation codes
this.tokenizedText.Update(time);
// change some demo showcase info based on keybinds
if (this.InputHandler.IsPressed(Keys.B))
this.drawBounds = !this.drawBounds;
if (this.InputHandler.IsPressed(Keys.T))
this.transform = !this.transform;
if (this.startIndex > 0 && this.InputHandler.IsDown(Keys.Left))
this.startIndex--;
if (this.startIndex < this.tokenizedText.String.Length && this.InputHandler.IsDown(Keys.Right))
this.startIndex++;
if (this.endIndex > 0 && this.InputHandler.IsDown(Keys.Down))
this.endIndex--;
if (this.endIndex < this.tokenizedText.String.Length && this.InputHandler.IsDown(Keys.Up))
this.endIndex++;
}
public override void Clear() {
base.Clear();
this.Game.Window.ClientSizeChanged -= this.OnResize;
}
private void OnResize(object sender, EventArgs e) {
// re-split our text if the window resizes, since it depends on the window size
// this doesn't require re-tokenization of the text, since TokenizedString also stores the un-split version
this.tokenizedText.Split(this.font, this.GraphicsDevice.Viewport.Width * TextFormattingDemo.WidthMultiplier, this.Scale, TextAlignment.Center);
}
}
}