UI issue

Hello everybody! Please share your opinion about the user interface. The moment has come when I need to make an interface system and I ran into a problem: it is impossible to crop the texture (text) on a limited rectangle. Imagine there is a parent rectangle within which I have to draw objects. But how would you do it? I know some tools: GraphicsDevice.ScissorRectangle; ViewPort; RenderTarget2D.

Why crop instead of scale?

Scaling with destination rectangle is the most common technique, as it does not crop and maintains the information.

Tangential recommendation for multi-language design:
To save myself from headaches later on, I size all of my buttons/text areas with the longest translation of all of the text, from various sources, into German and use the max size as the starting width, adding padding, for UI design.(German is the most verbose, character wise, of the common western languages, the idiomatic character expressions and compressed format of common eastern scripts are usually shorter than German, note: there are other language groups or expressions that exceed the established German length recommendation).

Onto the crop answers:

A RenderTarget will do it, any portion outside the virtual “screen” will be cutoff. This is rather slow due to the round trip to and from and to the video card and is not scalable.(see next paragraph for explanation)

The term [draw] call or batch[es] in the following paragraphs implies a separate spriteBatch.Begin() and End() pair and the corresponding composition and data transfer to the GPU.

Using Scissor mask will work, it must be done in a separate draw call, and many masks can be combined ahead of time into one, into a single call(assuming they are rendered at the same layer depth). This is faster than using RenderTarget since the calling thread is not stalled(see note 1) waiting on the return data from the GPU and a second compositing and draw call is not required for this operation.

Note 1: The thread is stalled upon read of the generated texture GPU from shared memory until the memory write is complete.


You could “crop” by making two copies of the background, one with a alpha hole in it. Then draw the full background - render text - draw the background with a hole. Of course if the text exceeds the background… Either use MeasureString() to trim the text or carefully plan out the draw order. An additional draw(or 5) to an existing call / batch has much less impact than multiple calls/batches to the GPU.

The fastest solution is to create a custom SpriteBatch.DrawString() in the MonoGame source and adjust the source rectangles and line breaks for the right and bottom limits, and early exiting the loop to minimizing the data copy SpriteFont.Texture2D to output Quad texture overhead and layer complexity.

But scaling with destination
rectangle is not possible for DrawString(), because there are no Rectangle parameters. This cannot be done using Monogame. Alternatively, I can try redefining the DrawString() method and somehow draw each letter using the Draw(Texture2D) method. I’ll try this option, thanks for the tip. I will unsubscribe about the result!

Implemented a method of drawing text with a bounding region that cuts the text. The code is not the best, but it will work for my job. The method is suitable for those who need to make a UI. Since the drawing method of the SpriteBatch class does not have the ability to crop the text, it would be great if the Monogame developers add a similar method to the SpriteBatch class.

P.S. Parameter “Font” wrapper over the class SpriteFont.

public static void DrawText(Font font, string text, Rectangle region, Color color, float depth)
{
    Point offset = new Point(); // Indent from each character

    for (int i = 0; i < text.Length; i++) // Iterate over each character
    {
        char character = text[i];

        switch (character)
        {
            case '\n':
                // New line indent
                offset.X = 0;
                offset.Y += font.SpriteFont.LineSpacing;
                continue;
            case '\r':
                continue;
        }

        var glyf = font.SpriteFont.Glyphs[character];
        var glyfSource = glyf.BoundsInTexture; // Rectangle source of character 

        Rectangle glyfRect = new Rectangle(region.Location + offset, glyfSource.Size); // Destination rectangle for character

        var overlapRect = Rectangle.Intersect(glyfRect, region); // Create overlapp

        if (overlapRect.Width == 0 && overlapRect.Height == 0) // Exit if current character rectangle not included in region rectangle
            continue;

        // Set new size destination and source for current character 
        glyfSource.Size  = overlapRect.Size;
        glyfRect.Size = overlapRect.Size;

        // Draw
        spriteBatch.Draw(font.SpriteFont.Texture, glyfRect, glyfSource, color, 0f, Vector2.Zero,  SpriteEffects.None, depth);

        
        offset.X += (int)glyf.WidthIncludingBearings; // Add X offset for next character
    }
}
1 Like

Very nice!

Not as performant as the original source, avoiding the unsafe pointers and calling spriteBatch.Draw() instead of directly creating a SpriteBatchItem.

But it works and is safer for general use.

Your code could allow for color and font changes, as well as shaking or growing/shrinking effects in a user context.

Great Job!

Most of those SpriteFont variables were internal variables not so long ago.

One comment on improving performance…

Should use a break; or return;so the rest of the characters in the text are not evaluated.

if (overlapRect.Width == 0 && overlapRect.Height == 0) // Exit if current character rectangle not included in region rectangle
            break; // or return;

Onto my original thought about the Rectangle:

My apologizes, I have been using my implementation of Monogame for so long…

This is my user space font scaling to rectangle method.

//class level variable
float fontScale = 0;   // reset to 0 if the rectangle Width or Height changes
public DrawScaledText(SpriteBatch sb, SpriteFont sf, string text, Rectangle Position, Color color)
{
    if (fontScale == 0)
    {
        Vector2 scaleSize = new Vector2(Position.Width,Position.Height) / sf.MeasureString(text);
        float fontScale = Math.Min(scaleSize.X, scaleSize.Y);
        // If growth is not wanted:
        // if(scaleSize.X > 1 && scaleSize.Y >  1)  fontScale = 1;
    }
    sb.DrawString(sf,text, new Vector2(Position.X,Position.Y), color,
   
    0, Vector2.Zero, new Vector2(fontScale, fontScale), SpriteEffects.None, 0, false);
// float rotation, Vector2 origin, Vector2 scale, SpriteEffects effects, float layerDepth, bool rtl)

// You can expose the rest of the parameters as needed.
}
1 Like

Thank you for the answer! I took a “note” for myself.

No problem, sorry about the delay.

Glad that I could point you in the right direction.