Introduction

At some point you might want to create your own component. This can be just to create some uniquely styled button but also a complex new component for displaying data or anything.

To do this, you need to subclass Control or other classes from guiSET. Here I'll give your a short guide through the main aspects, methods to implement or override and we'll also mention (some) basic concepts of this library. You might also want to read this article which goes deeper into core ideas and implementation of guiSET.

Alright, let's just dive in and start with examples. I recommend to read them in order to get all the necessary information for creating your own component. In the last part there is an overview of things to beware when creating certain types of components.

Outline:

First own component

To learn the basics, first we want to create a button that just displays its background color. We create a new class and let it extend Control - the base class for all guiSET components. Also let's add a constructor that allows setting the background and foreground color (NOTE: in Processing we get the impression there is a color type, but it is literally really just an int).

class ColorButton extends Control {
    ColorButton(int backgroundColor, int foregroundColor) { 
        setBackgroundColor(backgroundColor);
        setForegroundColor(foregroundColor);
    }
}

Now the IDE should complain that we do not implement a certain abstract render() method. The render() method is the key method that determines how our button will look like and all drawing takes place here. In the Control class there is a PGraphics object called pg on which we can draw. In case you are used to using off-screen PGraphics: don't worry about calling pg.beginDraw() or pg.endDraw() as this is already dealt with by the library. Best is to start with:

// It is not necessary to write "@Override" but better, because so you a) know you overrode 
// something here and b) can check if you got the name of the method right. You
// can't compile if this method does not override any method from its parent class.
@Override
void render() {
    drawDefaultBackground();
}

drawDefaultBackground() is already implemented in guiSET and will draw:

The border along with its color, radius and width is always drawn automatically somewhere else.

We could now come up with ideas to change the look of the button and add some stuff:

@Override
void render() {
    drawDefaultBackground();
    if (enabled) {
        pg.fill(getForegroundColor()); // set fill color to foregroundColor
    } else {
        pg.fill(color(0));        // set fill color to black if the component is disabled. 
    }
    pg.noStroke();                // no border 
    pg.rect(4, 4, getWidth()-8, getHeight()-8); // draw a rectangle in the center with foregroundColor
}

Now there will be a (somewhat random) rectangle inside the button that fills most of the area and is colored with the foregroundColor of the element. If the button is disabled however - the rectangle will be black.

Also, as this shall be a button, we might want to change the color when the mouse hovers over the component or presses it. To achieve this, we just set the hoverColor and pressedColor attribute to a different color. We can do this either manually or use the built-in method setStatusBackgroundColorsAutomatically(int backgroundColor) which not only sets background color but also hover and pressed color according to the given value. The latter two will be set a bit darker than the background (or lighter if the background is too dark).

class ColorButton extends Control {
    ColorButton(int backgroundColor, int foregroundColor) { 
        setStatusBackgroundColorsAutomatically(backgroundColor);
        setForegroundColor(getForegroundColor());
    }

    @Override
    void render() {
        drawDefaultBackground();
        if (enabled) {
            pg.fill(foregroundColor); 
        } else {
            pg.fill(color(0)); 
        }
        pg.noStroke();
        pg.rect(4, 4, getWidth()-8, getHeight()-8);
    }
}

Vertical label

This example can also be found in the library examples (as "Custom_Component") and here.

Now, next I want to create a fancy label that displays text not horizontally but vertically. For this purpose we subclass Label instead of Control because the latter has no functionality for text implemented. Instead, there is the (abstract) class TextBased which can be subclassed for any components that have text. Label also subclasses TextBased.

The easiest (or maybe only) way to achieve the rotated text is to use 2D-Transformations (nice article about that on the Processing website). We do:

class FancyLabel extends Label {
    FancyLabel(String text) {
        // this.text = text; // bad -> use setter (!):
        setText(text);
    }

    @Override
    void render() {
        drawDefaultBackground();
    
        pg.pushMatrix();               // saves the current position of the coordinate system
        pg.translate(getWidth(), 0);   // move by width to the left so rotation is right
        pg.rotate(radians(90));        // rotate by 90 degrees (around left top corner)
    
        drawDefaultText();             // guiSET default text drawing method (sets foregroundColor, textSize etc.)
        pg.popMatrix();                // restore the transformation to before when we called pushMatrix()
    }
}

Ok, now that kind of works but the text is just cut off. In the constructor we would better call setText(text) which in turn calls an autosize routine within the class. You can specify special rules for automatic resizing when text, font size or paddings have changed. All you need to do is implement the method autoWidth() and/or autoHeight() and return the desired width/height.

@Override
int autoWidth() {
    return (int)textHeight(getText()) + getPaddingLeft() + getPaddingRight();
}
@Override
int autoHeight(){
    return (int)textWidth(getText()) + getPaddingTop() + getPaddingBottom();
}

We set the width to the text height (might be multiple lines) and the height to the text width. Also, the paddings are added. The method setWidth(int) however calls update(), telling the component to render again next frame which might be unnecessary. Therefore we call setWidthNoUpdate(int). Same for height.

We might get one strange behavior though and that is min/maxWidth/Height: all setters for width and height constrain the given value between the set min and max value. These are reversed now for width and height and for the ultimate vertical label we also should swap minWidth and maxWidth etc. or even override some of the setters/getters.

The only problem is that drawDefaultText() expects the text to be drawn horizontally and uses the elements width and height to calculate the best position. Therefore, we need to swap width and height temporarily.

The full code:

class FancyLabel extends Label {
    FancyLabel(String text) {
        setText(text); // will call autosize 
    }

    @Override
    void render() {
        drawDefaultBackground();
    
        pg.pushMatrix();               // saves the current position of the coordinate system
        pg.translate(getWidth(), 0);   // move by width to the left so rotation is right
        pg.rotate(radians(90));        // rotate by 90 degrees (around left top corner)
    
        int tempWidth = getWidth();
        int tempHeight = getHeight();
        setHeightNoUpdate(tempWidth);  // Set height to width without updating
        setWidthNoUpdate(tempHeight);  // Set width to height without updating
    
        drawDefaultText();             // guiSET default text drawing method (sets foregroundColor, textSize etc.)
        pg.popMatrix();                // restore the transformation to before when we called pushMatrix()
    
        setHeightNoUpdate(tempHeight);
        setWidthNoUpdate(tempWidth);
    }
 
    // autosize is also called by setPadding(), setFontSize() and various others 
    @Override
    int autoWidth() {
        return (int)textHeight(getText()) + getPaddingLeft() + getPaddingRight();
    }
    @Override
    int autoHeight(){
        return (int)textWidth(getText()) + getPaddingTop() + getPaddingBottom();
    }
}

Vertical-Flow-Container

We'll now take a look at containers - components that group other elements. This example uses the actual implementation of VFlowContainer. This container ignores the position specified for its items (namely the x and y attributes) and just rows them up vertically one below the other.

We start off by creating constructors and overriding void render(). We override the method needsSortingByZ() because as just mentioned we apply some sort of a new layout and ignore the items x and y (This is important!). Items shall not be sorted because they will not overlap anyway and that could change the order. In render() we start off by calling drawDefaultBackground() which is always a good idea.

class VFlowContainer extends Container {
    public VFlowContainer() {
        this(100, 100);
    }

    public VFlowContainer(int width, int height) {
        super(width, height);
    }

    @Override
    protected boolean needsSortingByZ(){
        return false;
    }

    @Override
    protected void render() {
        drawDefaultBackground();
    }
}

Now the interesting part: to place one item below the other we keep track of the "used" space and initialize it with the containers paddingTop. Then iteratively we go through all items, render them and add the height of the item (and the margins) to usedSpace:

protected void render() {
    drawDefaultBackground();

    int usedSpace = getPaddingTop();

    for (Control c : items) {
        if (c.isVisible()) {
            renderItem(c, c.getMarginLeft() + getPaddingLeft(), usedSpace + c.getMarginTop());
            usedSpace += (c.getHeight() + c.getMarginTop() + c.getMarginBottom());
        }
    }
    drawDefaultDisabled();
}

A container should always use renderItem(Control item, int x, int y) to render its items because this prepared function applies a bunch of necessary settings. With x and y we specify the position of the item relative to the top left corner of the container.

Some further details include taking care of paddings and margins as well as calling drawDefaultDisabled() to gray out the container in case enabled is set to false.

Crucial things to be aware of

In general, when changing visual properties (like text or width), use the setter. I.e. the setters for width and height take care that neither exceeds maximum size nor is less than minimum size. Also some concepts like resizing and updating the graphics are handled by setters. Only sometimes you will want to change width/height without calling also update() and then you can use setWidthNoUpdate(int) and same for height.

And also (!), always, when writing a setter for a new property that affects how the component looks like on the screen, call update() in the setter !!

Implementing a new container

Your really should subclass the class Container or another container class! A lot of things a different for containers (such as event handling). Plus, containers need to render their items in some way.

In render(), use the provided method renderItem(Control item, int x, int y)

Also, if your container ignores the x and y coordinates of its items and places them in some different way (like the flow- and scroll containers do), be sure to override needsSortingByZ() and return false.

You also might want to override the fitContent() method which resizes the container to fit its content.

When overriding or creating new methods for adding items, use the already provided add(Control...c) method. Do not directly add to the ArrayList<Control> items!

Using mouse events

You can go ahead and override one or more of these internal class-specific methods:

It is recommended to call the super method in case the superclass does something important here:

@Override
protected void press(MouseEvent e) {
    super.press(e);

    // do your stuff 
}

Mind, you can call stopPropagation() in a mouse event routine to prevent further elements of the GUI to possibly receive the current mouse event.

Using key events

Just override:

Maybe you also to call the super method here too (especially when subclassing a textbox).

What really might be useful occasionally is the overridesFrameShortcuts() method. The standard return value for this is false (except for textboxes). If it returns true, then shortcuts registered at Frame will not be handled if an instance of your component is focused at the moment. I.e. it would be irrating when typing text into a textbox and every time you hit 'E', a shortcut that is by chance Shift+E is executed.

Methods you might want to override

render(), initialize(), int autoWidth(), int autoHeight(), fitContent(), int getAvailableWidth(), int getAvailableHeight(), overridesFrameShortcuts(), needsSortingByZ() internal event methods.