using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace Nuclex.UserInterface.Visuals.Flat { /// /// Locates the opening between characters in a string that is nearest /// to a user-defined location /// /// /// /// This is a class rather than a static class to prevent garbage production /// which would then have to be cleaned up again by the garbage collector. /// If you create an instance of it and keep reusing it, garbage and allocation /// will amortize. /// /// /// The method used to calculate the openings seems to be not terribly accurate. /// As of XNA 3.1, SpriteFonts don't do kerning, so the only thing left would be /// a variable space appended to the end of characters. This could be compensated /// for by always appending character with a known length for which no kerning is /// possible, for example, the pipe sign (|). /// /// internal class OpeningLocator { /// Initializes a new text opening locator public OpeningLocator() { this.textBuilder = new StringBuilder(64); } /// /// Locates the opening between two letters that is closest to /// the specified position /// /// Font that opening search will use /// Text that will be searched for the opening /// X coordinate closest to which an opening will be found /// The opening closest to the specified X coordinate public int FindClosestOpening(SpriteFont font, string text,float x) { // Measure the size of the whole string this.textBuilder.Remove(0, this.textBuilder.Length); this.textBuilder.Append(text); Vector2 textSize = font.MeasureString(this.textBuilder); // Run a binary search until to close in on the nearest opening int left = 0; float leftX = 0.0f; int right = text.Length; float rightX = textSize.X; for(;;) { // Is the provided coordinate outside of our search range? // -> Opening is to the far left or to the far right. if(x <= leftX) { return left; } else if(x >= rightX) { return right; } // Do we have only one character left to check? // -> Opening is either to its left or to its right. if(right - left <= 1) { if((x - leftX) <= (rightX - x)) { return left; } else { return right; } } // The position of the opening is still not absolutely clear, cut the string // in the middle of the search range so we can close in further. int middle = (right + left) / 2; this.textBuilder.Remove(middle, right - middle); textSize = font.MeasureString(this.textBuilder); // Depending on whether the searched-for position was on the left or right // of our cut, adjust appropriate side and prepare for another run if(x < textSize.X) { right = middle; rightX = textSize.X; } else { this.textBuilder.Append(text, middle, right - middle); left = middle; leftX = textSize.X; } } } /// Used by GetClosestOpening() to avoid garbage production private StringBuilder textBuilder; } } // namespace Nuclex.UserInterface.Visuals.Flat