GeonBit.UI: UI extension for MonoGame

Actually willmotil might be a good candidate to maybe help with the garbage.
I’ve tried to change the dictionaries to work with stringbuilder instead of string, and I’ve added helper functions for the enum to string conversion, but there are troubles with stringbuilder being keys in dictionaries it seems

Maybe I’ll try something else if I have time

Bummer. Well another approach is to ditch the strings completely and use enums instead (dictionary per state containing dictionaries per property). I sort of like the flexibility and simplicity of strings as keys for styling but at the bottom line if you’re adding new styles you are editing the code anyway, so you might as well just update an enum too.

Anyway regarding the dragging and positioning problem I think its fixed (merged with the master branch), I tried to reproduce the way you did and it seems ok, but if I misunderstood you and the bug still there let me know.

Thanks :slight_smile:

No it’s only important i suppose if it solves a problem that otherwise can’t be solved.
Or it makes using the project faster or simpler to use.

It is my code is huge and ugly and i just cant make it look pretty. Every time i rewrote it i just wanted to delete the whole class when i was done i haven’t tried in a long time and i was just about to when i saw your post. Maybe i wasn’t using the right pattern or I just suck at doing ui i guess.

Ill check it out.

Edit: much nicer then my messy class.

Might be boxing stuff it shouldn’t. Might be on the MonoGame side. Could be totally unrelated. SrtingBuilder is a class so it should work with a enum, though enums are special things in c#.

Not sure if any of this matters much unless your typing in a textbox or something which is probably no big deal anyways.

The bigger deal stuff is more monogame call related things, like when you have a fps or scroller and holding a key down to move creates garbage and your always holding a key down lol wasd ackk.

Then there is numerical to text displayed stuff which has special considerations for dynamically changing real time numerical displayed text, which you already know about.

Anyways just glancing it over… if you really wanted to not generate text garbage when typing

In InputHelper.cs → NewKeyTextInput → string lastCharPressedStr = key.ToString();
That’s probably generating garbage for every keystroke. There is probably other stuff.

In paragraphs.cs → WrapText is using string concatenation that always creates garbage that gets collected but only when it is actually called. like if the user is typing in a text box depending on how its displayed that could generate a ton of it ect…

Basically you would Probably would have to replace everything that uses strings to stringbuilder.
The above call alone could fill a post on how to do it with a stringbuilder though it would be easier. i dunno why that was the accepted suggestion on stackoverflow.

Provided MonoGame calls themselves are not generating garbage just pressing a key. Aside from boxing unboxing no no’s. Text is pretty much the single most garbage generating thing in most peoples apps and the hardest to eliminate collections caused by them but usually the severity of this depends on if its dynamically being altered and updated in real time or not.

So the idea is to not do it if and when its avoidable or reduce it to only updating strings when necessary.

Do users of a app created with this need to install the fonts ?

I would typically just allow the font to be set that is to be used in load content to the ui or a reference to it that the programmer adds to his project.

That could be a problem if users must install the fonts. Has this been tested ?

Anyways i cant get it to execute getting errors ill try again later.

Users of the application do not need the font, no. It is converted to a texture atlas when imported through the content pipeline as spritefont.

Only people who want to modify source code and recompile will need the font installed

Very good points, thanks! I will try it out next time I’ll work on it (unless one of you already have it implemented and want to pull request?)

@kosmonautgames is right you only need the fonts if you want to compile the code. But anyway if you want to build a MonoGame extension or a game engine or whatever else that users will need to compile and you don’t want to distribute fonts you can always change the default themes to use one of the default monospace fonts that come with Windows / Unix (fonts should be monospace).

Are you still working on this and updating problems?

Yeah recently I made a pretty big update and also added a NuGet package.

Are you thinking about using GeonBit.UI?

Well , I look at the 5 minute instal video and I tried for an hour and failed to use it. I think you need a foolproff tutorial for it.

There are people like me who does not know the difference between building and running a project or between debug and relase.

I will try nuget install but i think i will succed to fail at that too.

I was just rereading this i don’t know if you ever saw my wrapped StringBuilder class i posted a while back.

I should have posted it before. It might be of some use to you, in case your still dealing with garbage, even under stringbuilder.

In most of my screen shots i post there is basically no garbage. Primarily because of two things.

  1. MonoGame itself is awesome and generates little to no collectable garbage except in a select few edge cases.
  2. The below class… While it doesn’t address any problem with monogame, it instead address a problem with c# numerics tostring or tochar conversions creating garbage.

This is a thin wrapper on stringbuilder that basically augments stringbuilder to prevent garbage created by numeric to text conversion by c#. It simply does it instead of letting c# do it and create garbage. A couple extra temp stringbuilders handle edge case copying by holding onto references until they can be cleared during certain copy operations.

All the methods are proven so far at least in my tests, except the vector versions that are new and untested, so i commented them out. Some edge cases that are commented “//just append it” are not handled but they are pretty rare.

using System;
using System.Text;
using System.Collections.Generic;
using Microsoft.Xna.Framework;

namespace MyText
{
    public sealed class MgStringBuilder
    {
        private static char decimalseperator = '.';
        private static char minus = '-';
        private static char plus = '+';

        private static StringBuilder last;
        private StringBuilder sb;
        public StringBuilder StringBuilder
        {
            get { return sb; }
            private set { sb = value; last = sb; }
        }

        public int Length
        {
            get { return StringBuilder.Length; }
            set { StringBuilder.Length = value; }
        }
        public int Capacity
        {
            get { return StringBuilder.Capacity; }
            set { StringBuilder.Capacity = value; }
        }
        public void Clear()
        {
            Length = 0;
            sb.Length = 0;
        }

        // constructors
        public MgStringBuilder()
        {
            StringBuilder = StringBuilder;
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(int capacity)
        {
            StringBuilder = new StringBuilder(capacity);
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(StringBuilder sb)
        {
            StringBuilder = sb;
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }
        public MgStringBuilder(string s)
        {
            StringBuilder = new StringBuilder(s);
            if (sb == null) { sb = new StringBuilder(); }
            if (last == null) { last = new StringBuilder(); }
        }

        public static void CheckSeperator()
        {
            decimalseperator = Convert.ToChar(System.Globalization.CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
        }

        // operators
        public static implicit operator MgStringBuilder(StringBuilder sb)
        {
            return new MgStringBuilder(sb);
        }
        //public static implicit operator StringBuilder(MgStringBuilder msb)
        //{
        //    StringBuilder b = new StringBuilder().Append(msb.sb);
        //    return b;
        //}
        public static implicit operator StringBuilder(MgStringBuilder msb)
        {
            return msb.StringBuilder;
        }

        public static MgStringBuilder operator +(MgStringBuilder sbm, MgStringBuilder s)
        {
            sbm.StringBuilder.Append(s);
            return sbm;
        }

        public void AppendAt(int index, StringBuilder s)
        {
            int len = this.StringBuilder.Length;
            int reqcapacity = (index + s.Length + 1) - this.StringBuilder.Capacity;
            if (reqcapacity > 0)
                this.StringBuilder.Capacity += reqcapacity;

            for (int i = 0; i < s.Length; i++)
            {
                this.StringBuilder[i + index] = (char)(s[i]);
            }
        }
        public void Append(StringBuilder s)
        {
            int len = this.StringBuilder.Length;
            int reqcapacity = (s.Length + len) - this.StringBuilder.Capacity;
            //int reqcapacity = (s.Length + len +1) - this.StringBuilder.Capacity;
            if (reqcapacity > 0)
                this.StringBuilder.Capacity += reqcapacity;

            this.StringBuilder.Length = len + s.Length;
            for(int i = 0;i< s.Length;i++)
            {
                this.StringBuilder[i + len] = (char)(s[i]);
            }
        }
        public void Append(string s)
        {
            this.StringBuilder.Append(s);
        }
        public void Append(bool value)
        {
            this.StringBuilder.Append(value);
        }
        public void Append(byte value)
        {
            // basics
            int num = value;
            if (num == 0)
            {
                sb.Append('0');
                return;
            }
            int place = 100;
            if (num >= place * 10)
            {
                // just append it
                sb.Append(num);
                return;
            }
            // part 1 pull integer digits
            bool addzeros = false;
            while (place > 0)
            {
                if (num >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = num % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(short value)
        {
            int num = value;
            // basics
            if (num < 0)
            {
                // Negative.
                sb.Append(minus);
                num = -num;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 10000;
            if (num >= place * 10)
            {
                // just append it, if its this big, this isn't a science calculator, its a edge case.
                sb.Append(num);
                return;
            }
            // part 1 pull integer digits
            bool addzeros = false;
            while (place > 0)
            {
                if (num >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = num % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(int value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 1000000000;
            if (value >= place * 10)
            {
                // just append it
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }
        }
        public void Append(long value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            long place = 10000000000000000L;
            if (value >= place * 10)
            {
                // just append it,
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }
        }
        public void Append(float value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 100000000;
            if (value >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }

            // ok lets try again
            // nd > 0 wont let us see the decimal
            float nd = value - (float)(n);
            if (nd > -1 && nd < 1)
            {
                sb.Append(decimalseperator);
            }
            addzeros = true;
            //nd = value;
            float placed = .1f;
            while (placed > 0.00000001)
            {
                if (nd > placed)
                {
                    float modulator = placed * 10;
                    float val = nd % modulator;
                    float dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1f;
            }
        }
        public void Append(double number)
        {

            // basics
            if (number < 0)
            {
                // Negative.
                sb.Append(minus);
                number = -number;
            }
            if (number == 0)
            {
                sb.Append('0');
                return;
            }

            long place = 10000000000000000L;
            if (number >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(number);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(number);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }

            // the decimal part
            double nd = number - (double)(n);
            if (nd > 0 && nd < 1)
            {
                sb.Append(decimalseperator);
            }
            addzeros = true;
            //nd = number;
            double placed = .1;
            while (placed > 0.0000000000001)
            {
                if (nd > placed)
                {
                    double modulator = placed * 10;
                    double val = nd % modulator;
                    double dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1;
            }
        }
        //public void Append(Vector2 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(")");
        //}
        //public void Append(Vector3 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(",");
        //    Append(value.Z);
        //    Append(")");
        //}
        //public void Append(Vector4 value)
        //{
        //    Append("(");
        //    Append(value.X);
        //    Append(",");
        //    Append(value.Y);
        //    Append(",");
        //    Append(value.Z);
        //    Append(",");
        //    Append(value.W);
        //    Append(")");
        //}
        public void Append(Color value)
        {
            Append("(");
            Append(value.R);
            Append(",");
            Append(value.G);
            Append(",");
            Append(value.B);
            Append(",");
            Append(value.A);
            Append(")");
        }

        public void AppendTrim(float value)
        {
            // basics
            if (value < 0)
            {
                // Negative.
                sb.Append(minus);
                value = -value;
            }
            if (value == 0)
            {
                sb.Append('0');
                return;
            }

            int place = 100000000;
            if (value >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(value);
                return;
            }
            // part 1 pull integer digits
            int n = (int)(value);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    int modulator = place * 10;
                    int val = n % modulator;
                    int dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (int)(place * .1);
            }

            // ok lets try again
            float nd = value - (float)(n);
            sb.Append(decimalseperator);
            addzeros = true;
            //nd = value;
            float placed = .1f;
            while (placed > 0.001)
            {
                if (nd > placed)
                {
                    float modulator = placed * 10;
                    float val = nd % modulator;
                    float dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1f;
            }
        }
        public void AppendTrim(double number)
        {
            // basics
            if (number < 0)
            {
                // Negative.
                sb.Append(minus);
                number = -number;
            }
            if (number == 0)
            {
                sb.Append('0');
                return;
            }
            long place = 10000000000000000L;
            if (number >= place * 10)
            {
                // just append it, if its this big its a edge case.
                sb.Append(number);
                return;
            }
            // part 1 pull integer digits
            long n = (long)(number);
            bool addzeros = false;
            while (place > 0)
            {
                if (n >= place)
                {
                    addzeros = true;
                    long modulator = place * 10L;
                    long val = n % modulator;
                    long dc = val / place;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                place = (long)(place * .1);
            }

            // ok lets try again
            double nd = number - (double)(n);
            sb.Append(decimalseperator);
            addzeros = true;
            //nd = number;
            double placed = .1;
            while (placed > 0.001)
            {
                if (nd > placed)
                {
                    double modulator = placed * 10;
                    double val = nd % modulator;
                    double dc = val / placed;
                    sb.Append((char)(dc + 48));
                }
                else
                {
                    if (addzeros) { sb.Append('0'); }
                }
                placed = placed * .1;
            }
        }
        //public void AppendTrim(Vector2 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(")");
        //}
        //public void AppendTrim(Vector3 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(",");
        //    AppendTrim(value.Z);
        //    Append(")");
        //}
        //public void AppendTrim(Vector4 value)
        //{
        //    Append("(");
        //    AppendTrim(value.X);
        //    Append(",");
        //    AppendTrim(value.Y);
        //    Append(",");
        //    AppendTrim(value.Z);
        //    Append(",");
        //    AppendTrim(value.W);
        //    Append(")");
        //}

        public void AppendLine(StringBuilder s)
        {
            sb.AppendLine();
            Append(s);
        }
        public void AppendLine(string s)
        {
            sb.AppendLine();
            sb.Append(s);
        }
        public void AppendLine()
        {
            sb.AppendLine();
        }

        public void Insert(int index, StringBuilder s)
        {
            this.StringBuilder.Insert(index, s);
        }
        public void Remove(int index, int length)
        {
            this.StringBuilder.Remove(index, length);
        }

        public char[] ToCharArray()
        {
            char[] a = new char[sb.Length];
            sb.CopyTo(0, a, 0, sb.Length);
            return a;
        }
        public override string ToString()
        {
            return sb.ToString();
        }
    }
}
2 Likes

Sorry to hear that, I hope the NuGet will work out for you :slight_smile:
If you have any specific problems don’t hesitate to ask.

Cool stuff!

I think I solved most of the GC related issues in GeonBit.UI (Its still not 100% optimal but much better than before). I’m not doing any numeric-to-string conversions there (if I remember correctly) but your string builder might help me in another project I’m working on.

Do you have it on a git repo I can check out or should I just grab it from here?
Thanks :slight_smile:

No repo its just a stand alone class. Works as is… copy paste.
(change the namespace and its good to go).

sorry for the wall of text to explain it.

I didn’t think it was good enough yet for a repo, It looks ugly to me and i haven’t given a lot of thought to localization concerns i haven’t really cleaned it up since i made it, it really does need some more work, but…honestly it works very well for eliminating garbage.

All numeric conversion you do in c# creates garbage that gets collected.
The character conversion in c# for numerics to characters itself creates collections.
I actually posted this problem on user voice like a year ago.

this (1).ToString(); or (char)(1); is going to get garbage collected.
Including anything like the framerate positions ect…
score.ToString() no matter if it goes to StringBuilder string char whatever.
Its going to make the gc go “chomp chomp chomp boom collect”.
It’s easy enough to test / prove that yourself.

The class just gives you a way to never have to do that when using stringbuilder.

Basically its just a wrapper around StringBuilder but lets you append numbers separately so that they wont create garbage. I basically calculate the character itself by modulus for each decimal place from the value directly. mgSb.Append(1); won’t get collected were sb.Append(1); will.
I put just enough into it so it basically functions like stringbuilder on its own but you can pull out the stringbuilder by like StringBuilder sb = mgSb;.

I stuck the operator overloads in that i thought were safe. Which is enough to just pass it into any monogame method directly that takes a string builder so basically in most cases you can use it in place of stringbuilder.

I suppose you could use it with stringbuilder like…

sb.Append( mgsb.Append( frameRate ) );
but you can append text to it too so that’s kind of pointless.
You can if you like just rip out the conversion functions from it and make numeric converters for sb directly but it’s going to end up looking like the above anyways.

I basically just replace all my own string builder calls with the wrapper as there is no point in casting it back to a stringbuilder until i pass it to a monogame method and that is done implicitly via the operator.

like spriteBatch.DrawString( mgSbInstance, pos, color, ect…

1 Like

This UI looks very nice. I have been working on a UI for my own game as well. May I ask how you load your objects? I don’t have an editor yet and putting everything together in code leads to ugly mile long functions. That’s why I turned to XAML to load (or save) my layouts. That works so well that I also based the UI theme and even complete scenes for the 3D engine on XAML scripts embedded into the assembly.

Thanks :slight_smile: Unfortunately no, GeonBit.UI don’t support XAML atm. If you happen to write something like this (XAML for GeonBit.UI) let me know I’d love to integrate it.

I’m quite busy with my own UI, but what if I tell you that you only need to add about 10 lines of code in two static functions. That’s all. One function can load any valid XAML, the other one takes any serializable object and serializes it to XAML. You can use the second one to generate your first XAML files from the objects you have initialized the conventional way up to now. After that you can throw this code away and just load your objects from this XAML.

Look here: https://www.codeproject.com/Tips/188133/XamlServices-class-in-Net

The code I posted there works for files. That’s not practical. Change the loading method to use embedded resources and embed those XAML files into your assembly. You can’t save to an embedded resource, but that’s ok. You only need the save method as a crutch to get the first XAML files.

Hi Geon,

I’ve updated to the newest version (2.0.1) and the installation went fine with NuGet. I only had to change some stuff like UserInterface now being global, SCALE to GlobalScaleetc. and it ran with no problems.

I think some scale stuff is now handled differently, some buttons are now smaller than others, but I haven’t looked into why yet. Also some text now “runs away” at the beginning, but I’m sure that’s a mistake on my part (both things).

Either way, I think it’s easy to use and works fine.

One little thing: You use enum for almost everything, but when using UserInterface.Initialize(… theme) there is still a string (default “hd”), but without enums it’s hard to know what other themes there are if not looking at the documentation.

Also the shader files for HD and lowres are the same, maybe only having them once might prevent future bugs and save a little space and compilation time.

However, even though I can’t look at the files themselves, since they are “hidden” behind the compiled .dll now by default, i have huge troubles with garbage generation.

I have merely 3 buttons and 3 sliders, but the garbage generated is enormous, and that’s mainly due to using strings to read out configurations.

The last time I checked there was a lot of enums being stringed together and you then compare the string values and look for certain parts to continue.

If that is still the case, and performance profiling suggests it is (most garbage is created somewhere in GetStyleProperty), you could look into encoding these enum combinations differently, maybe in a struct or mini-class that combines several input parameters. Since enums are essentially ints you could also use some decimal or bit encoding / decoding.

1 Like

Glad to hear it went relatively smooth :slight_smile:

Could you elaborate more on the running text thing? Like, the text starts at wrong position and “jump” to its right place on first frame? If so with which entities? (had this problem in the past I though it was solved).

Also the sizing is a little odd, I did change the default themes font which might change texts a little, but not buttons… strange.

It was to allow adding new themes without changing the code, eg you can create your own folder of theme and just use it by name. However, point taken, maybe I’ll add a const for the two built-in themes or an enum and have function override.

That’s absolutely true, but I wanted to leave more freedom to custom themes. For example, maybe a theme where disabled entities are not greyed out but have different effect.

That’s very strange, I checked the demo again, with lots of entities etc, and the memory is constant with occasional GC calls:

I did found a serious memory leak with render targets which I just solved, but you’re not using them right?

There were other things as well last time we talked about it, most of them are solved. I added optimization against enums.toString() and some other places, but I have to admit I see no serious impact. :confused:

Can you elaborate more on your suggestion with the int encoding? Do you mean using a 2d array with indexes or something different?

Thanks for the useful (as always :slight_smile: ) feedback!

I will soon release a minor update that address some of the issues above, although I’m concerned that I didn’t see the GC madness you mentioned…

I know it’s not super helpful, but maybe you can spot a problem here:

I do nothing else with the UI. I commented out the text updates to make sure they are not part of the garbage.

All my toggles run away. Some buttons are larger than others, because I change their scale, I think previously it only changed text scale, so that is a mistake on me then.

What it looked like previously (same code) https://www.youtube.com/watch?v=jzW7Du0JfSk

Here is the code:

public void Initialize(GraphicsDevice graphics)
        {
            spriteBatch = new SpriteBatch(graphics);

            UserInterface.GlobalScale = 0.7f;
            UserInterface.ShowCursor = false;

            panel = new Panel(new Vector2(500,600), PanelSkin.Simple, Anchor.TopRight);
            panel.Draggable = false;
            UserInterface.AddEntity(panel);



            panel.AddChild(new Paragraph("Test UI"));
            panel.SetPosition(Anchor.TopRight, new Vector2(0,0));

            panel.AddChild(fps = new Paragraph("fps :"+GameStats.fps_avg));

            panel.AddChild(toggleShader = new CheckBox("Update Shader", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox) entity;
                GameSettings.g_UpdateShading = cb.Checked;
            };

            CheckBox seamFixToggle;
            panel.AddChild(seamFixToggle = new CheckBox("Fix Seams", Anchor.Auto, null, null, true));

            panel.AddChild(toggleShader = new CheckBox("Rotate Model", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                GameSettings.s_rotateModel = cb.Checked;
            };

            panel.AddChild(new LineSpace(1));
            
            Paragraph sliderValue = new Paragraph("Texture resolution: 512");
            panel.AddChild(sliderValue);
            

            Slider slider = new Slider(1, 12, SliderSkin.Default);
            slider.Value = 9;
            slider.StepsCount = 11;
            slider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider) entity;
                GameSettings.g_texResolution = (int) Math.Pow(2, slid.Value);
                //sliderValue.Text = "Texture resolution: " + GameSettings.g_texResolution;
            };
            panel.AddChild(slider);

            Paragraph environmentMap = new Paragraph("ambient intensity: 1.8");
            panel.AddChild(environmentMap);
            
            Slider envIntensitySlider = new Slider(0, 400, SliderSkin.Default);
            envIntensitySlider.StepsCount = 400;
            envIntensitySlider.Value = 180;
            envIntensitySlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                GameSettings.g_EnvironmentIntensity = slid.Value/100.0f;
                //environmentMap.Text = "ambient intensity: " + GameSettings.g_EnvironmentIntensity;
            };
            panel.AddChild(envIntensitySlider);

            Paragraph seamFixSteps = new Paragraph("seam dilate pixels: 1");
            panel.AddChild(seamFixSteps);

            Slider seamFixStepsSlider = new Slider(0, 6, SliderSkin.Default);
            seamFixStepsSlider.StepsCount = 6;
            seamFixStepsSlider.Value = 1;
            seamFixStepsSlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                GameSettings.g_SeamSearchSteps = slid.Value;
                //seamFixSteps.Text = "seam dilate pixels: " + GameSettings.g_SeamSearchSteps;
            };
            panel.AddChild(seamFixStepsSlider);


            seamFixToggle.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                GameSettings.g_FixSeams = cb.Checked;
                seamFixStepsSlider.Disabled = !cb.Checked;
            };

        }

Oh wow when you said running text you actually meant running text :joy: Never seen it happen before its so strange… And I use a lot of checkboxes.

Thank you for the info, I’ll take a look at it as soon as I’ll have the time (I’m at work atm) and hopefully will figure out the source of this checkbox shenanigans.

So I finally had time to work on this… Unfortunately, try as I might, I co
uldn’t reproduce the running away paragraphs.

I took your code and dumped it inside the GeonBit.UI example project:

`#region File Description
//-----------------------------------------------------------------------------
// This program show GeonBit.UI examples and usage.
//
// GeonBit.UI is an export of the UI system used for GeonBit (an open source 
// game engine in MonoGame) and is free to use under the MIT license.
//
// To learn more about GeonBit.UI, you can visit the git repo:
// https://github.com/RonenNess/GeonBit.UI
//
// Or exaplore the different README files scattered in the solution directory. 
//
// Author: Ronen Ness.
// Since: 2016.
//-----------------------------------------------------------------------------
#endregion

// using MonoGame and basic system stuff
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Generic;

// using GeonBit UI elements
using GeonBit.UI.Entities;
using GeonBit.UI.Entities.TextValidators;
using GeonBit.UI.DataTypes;

namespace GeonBit.UI.Example
{
    /// <summary>
    /// GeonBit.UI.Example is just an example code. Everything here is not a part of the GeonBit.UI framework, but merely an example of how to use it.
    /// </summary>
    [System.Runtime.CompilerServices.CompilerGenerated]
    class NamespaceDoc
    {
    }

    /// <summary>
    /// This is the main 'Game' instance for your game.
    /// </summary>
    public class GeonBitUI_Examples : Game
    {
        // graphics and spritebatch
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // all the example panels (screens)
        List<Panel> panels = new List<Panel>();

        /// <summary>
        /// 
        /// </summary>
      public  Button nextExampleButton;

        /// <summary>
        /// 
        /// </summary>
        public  Button previousExampleButton;

        // current example shown
        int currExample = 0;

        /// <summary>
        /// Create the game instance.
        /// </summary>
        public GeonBitUI_Examples()
        {
            // init graphics device manager and set content root
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Initialize the main application.
        /// </summary>
        protected override void Initialize()
        {         
            // create and init the UI manager
            UserInterface.Initialize(Content, BuiltinThemes.hd);
            UserInterface.UseRenderTarget = true;

            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // make the window fullscreen (but still with border and top control bar)
       /*     int _ScreenWidth = graphics.GraphicsDevice.Adapter.CurrentDisplayMode.Width;
            int _ScreenHeight = graphics.GraphicsDevice.Adapter.CurrentDisplayMode.Height;
            graphics.PreferredBackBufferWidth = (int)_ScreenWidth;
            graphics.PreferredBackBufferHeight = (int)_ScreenHeight;
            graphics.IsFullScreen = true;
            graphics.ApplyChanges();*/
            
            // init ui and examples
            InitExamplesAndUI();
        }
        
        /// <summary>
        /// Create the top bar with next / prev buttons etc, and init all UI example panels.
        /// </summary>        
        protected void InitExamplesAndUI()
        {

            UserInterface.GlobalScale = 0.7f;
            UserInterface.ShowCursor = false;

            Panel panel = new Panel(new Vector2(500, 600), PanelSkin.Simple, Anchor.TopRight);
            panel.Draggable = false;
            UserInterface.AddEntity(panel);


            CheckBox toggleShader = new CheckBox("aaa");


            panel.AddChild(new Paragraph("Test UI"));
            panel.SetPosition(Anchor.TopRight, new Vector2(0, 0));

            Paragraph fps;
            panel.AddChild(fps = new Paragraph("fps :"));

            panel.AddChild(toggleShader = new CheckBox("Update Shader", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
            };

            CheckBox seamFixToggle;
            panel.AddChild(seamFixToggle = new CheckBox("Fix Seams", Anchor.Auto, null, null, true));

            panel.AddChild(toggleShader = new CheckBox("Rotate Model", Anchor.Auto, null, null, true));
            toggleShader.Scale = 0.5f;
            toggleShader.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
            };

            panel.AddChild(new LineSpace(1));

            Paragraph sliderValue = new Paragraph("Texture resolution: 512");
            panel.AddChild(sliderValue);


            Slider slider = new Slider(1, 12, SliderSkin.Default);
            slider.Value = 9;
            slider.StepsCount = 11;
            slider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //sliderValue.Text = "Texture resolution: " + GameSettings.g_texResolution;
            };
            panel.AddChild(slider);

            Paragraph environmentMap = new Paragraph("ambient intensity: 1.8");
            panel.AddChild(environmentMap);

            Slider envIntensitySlider = new Slider(0, 400, SliderSkin.Default);
            envIntensitySlider.StepsCount = 400;
            envIntensitySlider.Value = 180;
            envIntensitySlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //environmentMap.Text = "ambient intensity: " + GameSettings.g_EnvironmentIntensity;
            };
            panel.AddChild(envIntensitySlider);

            Paragraph seamFixSteps = new Paragraph("seam dilate pixels: 1");
            panel.AddChild(seamFixSteps);

            Slider seamFixStepsSlider = new Slider(0, 6, SliderSkin.Default);
            seamFixStepsSlider.StepsCount = 6;
            seamFixStepsSlider.Value = 1;
            seamFixStepsSlider.OnValueChange = (Entity entity) =>
            {
                Slider slid = (Slider)entity;
                //seamFixSteps.Text = "seam dilate pixels: " + GameSettings.g_SeamSearchSteps;
            };
            panel.AddChild(seamFixStepsSlider);


            seamFixToggle.OnValueChange = (Entity entity) =>
            {
                CheckBox cb = (CheckBox)entity;
                seamFixStepsSlider.Disabled = !cb.Checked;
            };
        }

        /// <summary>
        /// Show next UI example.
        /// </summary>
        public void NextExample()
        {
            currExample++;
            UpdateAfterExapmleChange();
        }

        /// <summary>
        /// Show previous UI example.
        /// </summary>
        public void PreviousExample()
        {
            currExample--;
            UpdateAfterExapmleChange();
        }

        /// <summary>
        /// Called after we change current example index, to hide all examples
        /// except for the currently active example + disable prev / next buttons if
        /// needed (if first or last example).
        /// </summary>
        protected void UpdateAfterExapmleChange()
        {
            // hide all panels and show current example panel
            foreach (Panel panel in panels)
            {
                panel.Visible = false;
            }
            panels[currExample].Visible = true;

            // disable / enable next and previous buttons
            nextExampleButton.Disabled = currExample == panels.Count-1;
            previousExampleButton.Disabled = currExample == 0;
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // make sure window is focused
            if (!IsActive)
                return;

            // exit on escape
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
                Exit();

            // update UI
            UserInterface.Update(gameTime);

            // call base update
            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            // draw ui
            UserInterface.Draw(spriteBatch);

            // clear buffer
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // finalize ui rendering
            UserInterface.DrawMainRenderTarget(spriteBatch);

            // call base draw function
            base.Draw(gameTime);
        }
    }
}

`
And it looks like it should, without running text:

I tried random stuff like resizing window etc, still not happening :confused:

Is your code public perhaps, somewhere I can view it? Or maybe you got more ideas I can try?

Thanks!