GUIs

From Vintage Story Wiki
Jump to: navigation, search

Creating good graphical user interfaces is a challenging task - sometimes even the largest of companies mess it up.

In Vintage Story you currently need to formulate your GUIs in code, which requires acute spacial thinking. Hopefully someday we'll have a point&click GUI designer that takes off a lot of work. Until then, here are some of the essentials.

Starting out

You should encapsulate every GUI Dialog into its own class, albeit a dialog may consist of multiple windows - the vanilla character dialog for example has two - one for the players wearables and one for the player stats. There's a couple of base classes to make your live a bit easier.

Block Entity GUI

To create a GUI that's bound to a block entity, inherit from GuiDialogBlockEntity. In your block entity code you can then create and open that gui e.g. upon player interaction (example: Quern Block Entity, Quern Block Dialog)

HUD

To create a GUI element which is not interactable, inherit from HudElement.

General Purpose GUI

For any other uses, inerhit from the general purpose class GuiDialog, from which HudElement and GuiDialogBlockEntity also inherit from. You can override ToggleKeyCombinationCode to something like "yourAweseomeHotkeyCode" and use capi.Input.RegisterHotKey + capi.Input.SetHotKeyHandler to have your own keyboard key mapped to opening/closing your GUI (example: World Map)

GUI Basics

In general, you can pretty much build your own GUI system if you want to, by just overriding OnRenderGUI and render whatever you like. There's a multitude of overridable methods to handle mouse and keyboard inputs as well, see also the GuiDialog class on Github

If you want to use the vanilla GUI system, it's concept is merely a flat or hierarchical list of GUI elements placed at certain positions. The position is determined by an instance of ElementBounds. Let's have a closer look at it's properties:

ElementBounds

  • FixedX/FixedY: The absolute position where the element should be placed
  • FixedWidth/FixedHeight: The absolute width and height of the element
  • FixedPaddingX/FixedPaddingY: The absolute inner padding of the element
  • FixedMarginX/FixedMarginY: The absolute outer padding of the element
  • ParentBounds: The parent boundaries, in which this element resides in
  • ChildBounds: The child boundaries this element contains
  • Alignment: The alignment of the element. If set to None the FixedX/Y Position is used. For any other value the FixedX/Y values are ignored. For example when you used RightTop the element will always be at the right top most corner of its parent bounds. If you use RightFixed the element will be right aligned, but its Y-Position is determined by FixedY
  • HorizontalSizing/VerticalSizing: The sizing method to be used, can be either Fixed (default), Percentual or FitToChildren

GuiComposer

This is the component that builds and manages your GUI elements for you. You can create a composer via the client api: capi.Gui.CreateCompo(dialogName, bounds). You have to supply it with a unique identifier and the overall dialog bounds. Once you have a GUIComposer instance you can chain-add elements. Here's a small example:

private void SetupDialog()
{
    // Auto-sized dialog at the center of the screen
    ElementBounds dialogBounds = ElementStdBounds.AutosizedMainDialog.WithAlignment(EnumDialogArea.CenterMiddle);

    // Just a simple 300x300 pixel box
    ElementBounds textBounds = ElementBounds.Fixed(0, 0, 300, 300);
        
    SingleComposer = capi.Gui.CreateCompo("myAwesomeDialog", dialogBounds)
        .AddStaticText("This is a piece of text at the center of your screen - Enjoy!", CairoFont.WhiteDetailText(), textBounds)
        .Compose()
    ;
}

Some explanations:

  • ElementStdBounds: Contains a bunch of often used element bounds configurations. See also ElementStdBounds on Github.
  • ElementBounds.Fixed(0, 0, 300, 300): Create a new bounds instance with fixedX/Y at 0/0 and a fixed widt/height of 300/300 pixels.
  • CairoFont.WhiteDetailText(): Create a new font configuration based on a often used size and color, in this case white with font size 14
  • SingleComposer: This property is defined in the GuiDialog class. Its a getter/setter to the Composers dictionary. It contains all the "windows" of this dialog which are then rendered / handled by this GuiDialog instance

Fancying it up

This is of course the absolute minimum example that will only show some text. Let's add a title bar and a dialog background:

private void SetupDialog()
{
    // Auto-sized dialog at the center of the screen
    ElementBounds dialogBounds = ElementStdBounds.AutosizedMainDialog.WithAlignment(EnumDialogArea.CenterMiddle);

    // Just a simple 300x100 pixel box with 40 pixels top spacing for the title bar
    ElementBounds textBounds = ElementBounds.Fixed(0, 40, 300, 100);

    // Background boundaries. Again, just make it fit it's child elements, then add the text as a child element
    ElementBounds bgBounds = ElementBounds.Fill.WithFixedPadding(GuiStyle.ElementToDialogPadding);
    bgBounds.BothSizing = ElementSizing.FitToChildren;
    bgBounds.WithChildren(textBounds);

    SingleComposer = capi.Gui.CreateCompo("myAwesomeDialog", dialogBounds)
        .AddShadedDialogBG(bgBounds)
        .AddDialogTitleBar("Heck yeah!", OnTitleBarCloseClicked)
        .AddStaticText("This is a piece of text at the center of your screen - Enjoy!", CairoFont.WhiteDetailText(), textBounds)
        .Compose()
    ;
}

private void OnTitleBarCloseClicked()
{
    TryClose();
}

This covers some of the most basic parts. There is a grand amount of various pre-built UI elements that each come with their own argument list that are in dire need of Documentation. Here's an overview of some of the more commonly used ones

Dialog / Graphics

  • .AddShadedDialogBG: Draws a pretty background and dialog border
  • .AddDialogTitleBar: Draws a title bar with a close button and a button to move around the dialog
  • .AddInset: Adds a darkened section with a inset border around it

Text

  • .AddStaticText: Add a static snippet of text
  • .AddDynamicText: Add a static snippet of text that can be set to other texts without the need to redraw the whole dialog
  • .AddRichtext: Same as .AddDynamicText but allows use of VTML - a minimalist version of HTML code
  • .AddHoverText: When the mouse cursor moves over the element boundaries, will show supplied text as a tooltip

UI Control/Input

  • .AddButton: Adds a clickable button
  • .AddDropDown: Adds a drop down element
  • .AddHorizontalTabs: Adds horizontally aligned tabs, like the ingame chat window has
  • .AddVerticalScrollbar: Adds a vertical scrollbar
  • .AddTextInput: Adds an single line editable text field
  • .AddNumberInput: Adds an editable text field built for entering numbers
  • .AddTextArea: Adds multiple line editable text field

Tables/Grids/Inventory

Other

  • .AddIf/.EndIf: Can be used to conditionally add certain elements (but you can also just split up your creation code for more fine grained control)
  • .BeginClip/.EndClip: Used in combination with a scrollbar to cut away oversized content, such as in the creative inventory
  • .AddStaticCustomDraw: Lets you define your own drawing code to be added to the GUI

Examples

A simple standard dialog, triggered by the keyboard key 'U'

public class GuiDialogAnnoyingText : GuiDialog
{
    public override string ToggleKeyCombinationCode => "annoyingtextgui";

    public GuiDialogAnnoyingText(ICoreClientAPI capi) : base(capi)
    {
        SetupDialog();
    }

    private void SetupDialog()
    {
        // Auto-sized dialog at the center of the screen
        ElementBounds dialogBounds = ElementStdBounds.AutosizedMainDialog.WithAlignment(EnumDialogArea.CenterMiddle);

        // Just a simple 300x300 pixel box
        ElementBounds textBounds = ElementBounds.Fixed(0, 40, 300, 100);

        // Background boundaries. Again, just make it fit it's child elements, then add the text as a child element
        ElementBounds bgBounds = ElementBounds.Fill.WithFixedPadding(GuiStyle.ElementToDialogPadding);
        bgBounds.BothSizing = ElementSizing.FitToChildren;
        bgBounds.WithChildren(textBounds);

        // Lastly, create the dialog
        SingleComposer = capi.Gui.CreateCompo("myAwesomeDialog", dialogBounds)
            .AddShadedDialogBG(bgBounds)
            .AddDialogTitleBar("Heck yeah!", OnTitleBarCloseClicked)
            .AddStaticText("This is a piece of text at the center of your screen - Enjoy!", CairoFont.WhiteDetailText(), textBounds)
            .Compose()
        ;
    }

    private void OnTitleBarCloseClicked()
    {
        TryClose();
    }
}

public class AnnoyingTextSystem : ModSystem
{
    ICoreClientAPI capi;
    GuiDialog dialog;

    public override bool ShouldLoad(EnumAppSide forSide)
    {
        return forSide == EnumAppSide.Client;
    }
        
    public override void StartClientSide(ICoreClientAPI api)
    {
        base.StartClientSide(api);

        dialog = new GuiDialogAnnoyingText(api);

        capi = api;
        capi.Input.RegisterHotKey("annoyingtextgui", "Annoys you with annoyingly centered text", GlKeys.U, HotkeyType.GUIOrOtherControls);
        capi.Input.SetHotKeyHandler("annoyingtextgui", ToggleGui);
    }

    private bool ToggleGui(KeyCombination comb)
    {
        if (dialog.IsOpened()) dialog.TryClose();
        else dialog.TryOpen();

        return true;
    }
}


TBD

More to come here in the future.