Getting Started with GUIs

From Team Wizardry Docs
Jump to navigation Jump to search

The old system is somewhat simpler.

  • At its core the framework is about "components." A component could render a graphic, like ComponentSprite, or it could be purely a container, like ComponentVoid.
  • The simplest components will be those that just render something, as those are basically just an object with a draw method. The more complex ones will be the containers, as these will have many components nested within them.
  • Each component defines a coordinate space by having a position, rotation, and scale. Because of this any rendering within a component happens relative to (0, 0) and doesn't care about scaling or rotation.
  • A component's pos is its position within its parent, with the child's origin lying on that point. The component will scale and rotate around that point.
  • A component's size is its size within its own context. This means that a 100x100px component will still have a size of (100, 100) after it's been scaled down by 50%
  • Each component keeps track of the mouse position relative to its coordinate space. For example if you have some component "A" with a mouse position of (25, 25), and within it you have some component "B" with a scale of 50%, the mouse position in "B" will be (50, 50)
  • Each component will receive input events, and these are posted to its personal event bus (which works very similarly to forge's)

The new version adds a wrinkle to this: The rendering and interactivity of components are now split. There are GuiLayers, which provide purely rendering (they have no mousePos, input events, etc.), and there are GuiComponents, which extend GuiLayer with interactive functionality (mousePos, input events, etc.)

Other notable changes:

  • The API is much cleaner
  • Coordinate spaces are now properly handled and converting points from one coordinate space to another is dead easy
  • The "mouseOver" handling is much better (basically it's what prevents you clicking a button that's covered by something else)
  • Did I mention the API is cleaner?
  • GuiComponent and GuiLayer are no longer abstract, meaning there's no need for ComponentVoid
  • There is support for dynamic layout using a UIKit-esque layoutChildren method that is automatically run if the size of a component has changed or its children have moved.
  • The window manager is a thing

Now for some examples of the new system. The java code examples aren't tested but should be correct. (They're direct translations of the Kotlin versions, which were tested)

One of the simplest GUIs you could make is this:

class GuiTestSprite : GuiBase() {
    init {
        main.size = vec(100, 100)
        val sprite = Sprite(ResourceLocation("textures/blocks/glass_yellow.png"))
        val c = ComponentSprite(sprite, 25, 25, 50, 50)
        main.add(c)
    }
}
public class GuiTestSprite extends GuiBase {
    public GuiTestSprite() {
        super();
        main.setSize(new Vec2d(100, 100));
        Sprite sprite = new Sprite(new ResourceLocation("textures/blocks/glass_yellow.png"));
        ComponentSprite c = new ComponentSprite(sprite, 25, 25, 50, 50);
        getMain().add(c);
    }
}

Breaking it down:

class GuiTestSprite : GuiBase() {
public class GuiTestSprite extends GuiBase {
    public GuiTestSprite() {
        super();

Create a new liblib gui (GuiBase is a subclass of GuiScreen)

    init {
        main.size = vec(100, 100)
        main.setSize(new Vec2d(100, 100));

In the initializer (constructor) we set the main content area's size to (100, 100). The GUI will automatically center this component in the screen for you. The main component doesn't do anything, it's simply there as a container for the rest of your UI so you can use relative coordinates and don't have to do pos = new Vec2d(screenX + 10, screenY + 10). The GuiBase will also downscale this component, so if the user's GUI scale is 3x but your main component is too large to fit in their screen LibLib will opt to bump the GUI down to 2x instead of extending past the edges of the screen.

        val sprite = Sprite(ResourceLocation("textures/blocks/glass_yellow.png"))
        Sprite sprite = new Sprite(new ResourceLocation("textures/blocks/glass_yellow.png"));

Now to the fun part. We create a new Sprite, which is basically a drawable texture (or part of a texture). The sprite system is used so you can separate the U/V/W/H texture coordinates from your GUI code. Instead the sprite positions/animations/slicing are defined in the texture's .mcmeta file.

        val c = ComponentSprite(sprite, 25, 25, 50, 50)
        ComponentSprite c = new ComponentSprite(sprite, 25, 25, 50, 50);

Next we create a ComponentSprite, which is simply a component that renders a sprite over its size (the corners of the texture at the corners of the component). We pass the sprite we want rendered and initialize the x/y/width/height of the component. This component will cover the middle of the 100x100 main component with 25px of space on each side.

        main.add(c)
        getMain().add(c);

Here we simply add our new component to the GUI's main component. After this point it will just render. Unlike MC GUIs this is a retained-mode, fire-and-forget system. Add a component and it'll render itself until the end of time. You can keep a reference to the component to be able to modify or remove it, but for most GUI elements that's not necessary.

    }
}
    }
}

The next example will be interactive. The GUI will display a piece of text over a translucent background. The text will be changed to reflect what click event was fired and the background will become fully opaque when the mouse is pressed and go back to translucent when it is released.

class GuiTestClickEvents : GuiBase() {
    init {
        main.size = vec(100, 100)

        val c = ComponentRect(25, 25, 50, 50)
        c.color = Color(255, 0, 0, 127)
        main.add(c)
public class GuiTestClickEvents extends GuiBase {
    public GuiTestClickEvents() {
        super();
        getMain().setSize(new Vec2d(100, 100));

        ComponentRect c = new ComponentRect(25, 25, 50, 50);
        c.setColor(Color(255, 0, 0, 127));
        getMain().add(c);

Similar to before we create a component that covers the center of the screen. This time however we use ComponentRect, which is just a quad with a flat color. In this case the color is 50% transparent red. Like before we add the new component to the GUI's main component.

        val text = ComponentText(0, 20)
        c.add(text)
        ComponentText text = new ComponentText(0, 20);
        c.add(text);

Now we get to some new stuff. We create a text component which—as the name would suggest—draws text, and initialize its position to (0, 20). This time however we don't add the text component to the GUI's main component, we add it to the ComponentRect we created earlier. Doing this means that the ComponentText will draw relative to the rect's origin. Because of this the text will actualy appear against the left edge of the rect and 20px from the top edge of it.

        c.BUS.hook(GuiComponentEvents.MouseClickEvent::class.java) { event ->
            text.text = "click"
        }
        c.BUS.hook(GuiComponentEvents.MouseClickEvent.java, (event) -> {
            text.setText("click");
        });

Now we start using events. The first event we hook into is the mouse click event. This event fires any time the cursor is pressed over the component and then released while still over the component and has a property that contains the button that was clicked. Other events can have many fields, some are mutable properties allowing event handlers to influence the result (e.g. clamping the new position of a drag component to a single line, like in the Pastry slider) and some subclass EventCancelable and so can be canceled (such as the StateChangeEvent in the Pastry toggle buttons).

        c.BUS.hook(GuiComponentEvents.MouseClickOutsideEvent::class.java) { event ->
            text.text = "click outside"
        }
        c.BUS.hook(GuiComponentEvents.MouseClickDragInEvent::class.java) { event ->
            text.text = "click drag in"
        }
        c.BUS.hook(GuiComponentEvents.MouseClickDragOutEvent::class.java) { event ->
            text.text = "click drag out"
        }
        c.BUS.hook(GuiComponentEvents.MouseClickOutsideEvent.java, (event) -> {
            text.setText("click outside");
        });
        c.BUS.hook(GuiComponentEvents.MouseClickDragInEvent.java, (event) -> {
            text.setText("click drag in");
        });
        c.BUS.hook(GuiComponentEvents.MouseClickDragOutEvent.java, (event) -> {
            text.setText("click drag out");
        });

These events are similar to the first. MouseClickOutsideEvent fires when the mouse goes down outside the component, then back up outside the component. DragIn is down outside, up inside. DragOut is down inside, up outside.

        c.BUS.hook(GuiComponentEvents.MouseDownEvent::class.java) { event -> 
            c.color = Color(255, 0, 0, 255)
        }
        c.BUS.hook(GuiComponentEvents.MouseUpEvent::class.java) { event -> 
            c.color = Color(255, 0, 0, 127)
        }
        c.BUS.hook(GuiComponentEvents.MouseDownEvent.java, (event) -> {
            c.setColor(new Color(255, 0, 0, 255);
        });
        c.BUS.hook(GuiComponentEvents.MouseUpEvent.java, (event) -> {
            c.setColor(new Color(255, 0, 0, 127);
        });

These two events allow the user to hook directly into the input events, as in this case where we change the background color when the mouse goes down/up.

    }
}
    }
}