Skip to content

Example Mod: Text Editor (Part 1)

Zach Hembree edited this page May 15, 2021 · 11 revisions

Applicable Version(s): 0.9.9

Notice: This tutorial is out of date and will not be updated until July.

In this series, we'll be working through the implementation of a simple text editor using this framework. By the time, we've finished we'll have created a windowed text editor that will allow us to enter, edit and reformat text in just about any format the framework is capable of producing, and in doing so, I'll be introducing most, if not all, of the framework's major systems.

Overview:

This tutorial will be divided into the following parts:

  • Part 1: Create a new window with an interactable textbox.
  • Part 2: Add keybind for opening/closing the window and add scrollbars to the textbox.
  • Part 3: Add a toolbar for controlling the text box's formatting.
  • Part 4: Add the logic needed to interpret and apply the formatting specified by the toolbar.

At the end of each part in this series, we will have a functional mod that can be loaded and tested in-game, and at each successive stage, we'll be adding more and more functionality. By the end of part 4, we'll have something resembling some sort of useful mod. It won't be anything you'd want to use for anything, but it's useful as a learning experience or technical demonstration.

For anyone interested, you can find a copy of the finished project for each part in the series here.

Namespaces Being Used:

using RichHudFramework.Client;
using RichHudFramework.UI.Client;
using RichHudFramework.UI;
using VRage.Game;
using VRage.Game.Components;
using VRageMath;

Class Organization:

For the sake of brevity and to better facilitate commentary, code not immediately relevant to any given step in this project will be omitted. To help avoid any confusion, the code block below shows how the types for this part of the tutorial will be organized, and as we progress, we'll be filling in the members for each type.

namespace TextEditorExampleMod
{
    public sealed class ExampleModMain : MySessionComponentBase
    { }

    internal class TextEditor : WindowBase
    {
        private class ScrollableTextBox : HudElementBase
        { }
    }
}

If you prefer, you can find the completed code for each part in this tutorial [here] and follow along using that.

Steps:

  1. Create a New Window Class:

    Declare a new class deriving from WindowBase. All HUD element base classes require a parent element (IHudParent) to be passed into the constructor, so we'll need to add a parameter for that to our constructor as well.

    public class TextEditor : WindowBase
    {
        public TextEditor(IHudParent parent = null) : base(parent)
        { }
    }

    Most of the time you'll be assigning the parent element via the constructor, but in some cases, you'll find yourself needing to instantiate an element before its parent, and in those cases, you can just pass in null.

  2. Create a Customized TextBox:

    Next, we'll be declaring a custom TextBox class. We could just use the TextBox as it is, but we'll be adding scrollbars later on, and doing that inside the main window class would get a bit messy.

    // When creating new HUD elements, you'll almost always be deriving from
    // HudElementBase.
    private class ScrollableTextBox : HudElementBase
    {
        public readonly TextBox text;
    
        public ScrollableTextBox(IHudParent parent = null) : base(parent)
        {
            // Passing this into the constructor will set this instance as 
            // the parent of the new TextBox.
            text = new TextBox(this)
            {
                // All text entered will be white with a size of 1f,
                // left-aligned and will use the default SE font.
                Format = GlyphFormat.White,
                // This is a text editor, I don't want the text centered.
                VertCenterText = false,
                // Allows the text box size to be set manually 
                // (or via DimAlignment)
                AutoResize = false,
                // Match the size of the TextBox to the size of its parent
                // minus its parent's padding.
                DimAlignment = DimAlignments.Both | DimAlignments.IgnorePadding,
                // Enable text wrapping.
                BuilderMode = TextBuilderModes.Wrapped,
            };
        }
    }

    At this point, you might be wondering "hey, didn't you forget to set the position?" Good question, and no, no I did not!

    By default, all HUD elements are positioned relative to the center of their parent element. This starting position is called the element's Origin. The Origin property cannot be modified externally; to change an element's position you must change its Offset property.

    To get an element's absolute position you can just find the sum of the Origin and Offset properties or check the Position property, but most of the time, you won't need to know an element's exact location. Most of the time, you'll be working exclusively with Offset.

  3. Add the TextBox to the Window:

    Now that's done, we can add our new text box to the editor window. WindowBase is a little more complicated than HudElementBase; it already has three member elements: the header, body and the border. We could just parent our text box to the window itself, but then we'd have to start positioning things manually and I'd like to avoid that as long as possible.

    The body seems like a good candidate; if I parent the TextBox to that, all I'll need to do is set the DimAlignment to match its size.

    private readonly ScrollableTextBox textBox;
    
    public TextEditor(IHudParent parent = null) : base(parent)
    {
        // Initialize our new text box and parent it to the window's body
        textBox = new ScrollableTextBox(body)
        {
            // Match the text box size to the size of the window's body less 
            // padding
            DimAlignment = DimAlignments.Both | DimAlignments.IgnorePadding,
        };
    }
  4. Configure the Styling of The Window:

    By default, the window's header, body and border are all white, and if that weren't bad enough, the default text color is white too! Needless to say, we're going to need to change that. Fortunately, that's a quick fix...

    public TextEditor(IHudParent parent = null) : base(parent)
    {
        // Initialize our new text box and parent it to the window's body
        textBox = new ScrollableTextBox(body)
        {
            // Match the text box size to the size of the window's body less 
            // padding
            DimAlignment = DimAlignments.Both | DimAlignments.IgnorePadding,
        };
    
        // Window styling:
        BodyColor = new Color(41, 54, 62, 150);
        BorderColor = new Color(58, 68, 77);
    
        Header.Format = new GlyphFormat(GlyphFormat.Blueish.Color, TextAlignment.Center, 1.08f);
        header.Height = 30f;
    
        HeaderText = "Example Text Editor";
    }

    Oh, and if you were wondering what it would look like if we left all the colors at their defaults... enter image description here

    UX at its finest, no?

  5. Instantiate the Window:

    We're almost done; all we need to do now is instantiate the editor in our main class. For the sake of simplicity, we'll be using the example main class as a template.

    HudMain.Root is the root parent element for all UI elements, including those registered in other mods. You could draw and update your HUD elements manually or create a custom root element, but that's beyond the scope of this tutorial.

    [MySessionComponentDescriptor(MyUpdateOrder.NoUpdate)]
    public sealed class ExampleModMain : MySessionComponentBase
    {
        private TextEditor editorWindow;
    
        public override void Init(MyObjectBuilder_SessionComponent sessionComponent)
        {
            RichHudClient.Init("Text Editor Example", HudInit, ClientReset);
        }
    
        private void HudInit()
        {
            // Initialize and register window to HudMain.Root. All elements are visible
    	// by default, so we shouldn't need to do anything else.
    	editorWindow = new TextEditor(HudMain.Root)
    	{
    	    Size = new Vector2(500f, 300f),
    	};
        }
    
        public override void Draw()
        {
    	// All elements start with a scale of 1f by default. Doubling that scale to 2f
    	// would double its size and offset on both axes. HudMain.ResScale returns
    	// the scaling needed to compensate for changes in apparent size on 
    	// high-resolution (> 1080p) displays due to increased pixel density.
    	if (RichHudClient.Registered)
            {
    	    editorWindow.Scale = HudMain.ResScale;
            }
        }
    
        private void ClientReset()
        { }
    }

    Each child element of type HudElementBase inherits and compounds it's parent's scaling. If element B is parented to element A and element A has a scale of .5f and element B has an internal scale of 2f, then element B's effective scale will be 1f.

    For this reason, resolution scaling should only be applied at the highest level in your UI's element hierarchy, and if you're doing otherwise, you better make sure you have a well thought out reason for doing so.

Conclusion:

At this point, we should have a working mod and it should just be a matter of copying the folder into %AppData%\Roaming\SpaceEngineers\Mods and loading a world with it in the mod list.

If you've done everything correctly, you should see something like this on startup:

enter image description here

The window will behave pretty much like every other window you've ever seen. You can drag it around and resize it, but that's about it at this point. As for the text box, you should be able to type in text, move the caret around using the arrow keys, and you'll also be able to use the usual Ctrl+A/C/X/V binds for selecting/copying/cutting/pasting text.