The Component System

If you want to have text GUI controls in your application then Zircon has you covered with the Component abstraction.

Components in Zircon are text GUI elements which come in 3 flavors: there are components which you can use to display content, like Labels, Paragraphs and ListItems. You also have Components which can be used for interaction, such as Buttons, ToggleButtons and RadioButtons. Apart from the ones above there are also Containers which can have other Components as their children.

How Components Work

Components work in a similar way as Layers do. They have graphical content which is backed by a TileGraphics object and they can also be moved around like Layers. The main difference between them is that their structure is hierarchical: they form a tree. Components serve as leaf objects in this tree and Containers can have child Components.

To read more about Layers you can go to the relevant documentation page.

This is how the Component interface looks like:

interface Component : ComponentEventSource, ComponentProperties, Focusable, Movable, UIEventSource {

    val absolutePosition: Position

    val relativePosition: Position

    val contentOffset: Position

    val contentSize: Size

    val componentState: ComponentState

    val currentStyle: StyleSet

    var componentStyleSet: ComponentStyleSet
}

This might look rather overwhelming at first but it will make sense soon. Let’s take a look at what properties are there in a Component. absolutePosition is the Position of a Component relative to the top left corner of the Screen. Conversely relativePosition is the Position of the Component relative to its parent Container. contentOffset denotes the Position where the actual content of the Component starts relative to the top left corner of it. This will be explained later when we talk about component decorations. contentSize is the size of the area where the contents of the Component will be drawn. componentState contains the possible states a Component can be in:

enum class ComponentState {
    DEFAULT,
    HIGHLIGHTED,
    FOCUSED,
    DISABLED,
    ACTIVE
}

What they mean is:

  • All Components are in the DEFAULT state when they are created and the user is not interacting with them
  • A Component is HIGHLIGHTED when you move your mouse over it
  • Whenever you navigate to a Component using the navigation keys ([Tab] and [Shift]+[Tab] by default) they become FOCUSED. Only one Component can be focused at any given time.
  • Components can be DISABLED. In this state they won’t receive any events and they will also be visually different (grayed out).
  • A Component is ACTIVE when you are pressing the mouse over it, or if you are pressing the activation key ([Spacebar] by default).

A Component also becomes focused when you click on it.

A ComponentStyleSet holds StyleSets for every possible ComponentState. Whenever a Component’s state changes it will be redrawn with the appropriate StyleSet from the ComponentStyleSet.

Apart from being disabled Components can also be hidden, they can have their own tilesets for drawing them. Take a look at TilesetOverride to see how you can change tilesets.

Each Component will decide on its own how it uses the ComponentStyleSet for drawing. This enables Zircon to have Components which are visually consistent:

All Components

Components also support applying ColorThemes to them. You can read more about it here. They are a simple way of applying consistent styling to all your Components. Take a look at this example:

Switching Themes

Event Handling

Handling events work in a similar way as with TileGrids. In fact Components implement UIEventSource so you can listen to all events you have on TileGrids. The only difference is that you’ll get only those events which happened in the context of the Component. For example you will only get MOUSE_MOVED events if the mouse is over the Component. Apart from regular events you can also work with ComponentEvents.

If you’re not familiar with how the event system works check out the relevant documentation page here.

There are 4 kinds of ComponentEvents you can listen to:

enum class ComponentEventType : UIEventType {
    FOCUS_GIVEN,
    FOCUS_TAKEN,
    ACTIVATED,
    DEACTIVATED
}
  • FOCUS_GIVEN will be called whenever the Component gets focus
  • FOCUS_TAKEN is the opposite of FOCUS_GIVEN
  • The ACTIVATED event is fired when the activation key is pressed ([Spacebar] by default), or if you press on the Component.
  • DEACTIVATED is fired when you release the key, or the button

Listening to these events is pretty straightforward:

screen.addComponent(button()
        .withText("Click me!"))
        .onActivated(Functions.fromConsumer((event) -> {
            System.out.println("Hey, I was clicked.");
        }));

There are also methods for listening to the rest of the component events (onFocusGiven, onFocusTaken and onDeactivated).

Creating Components

We’ve learned a lot about Components but we don’t yet know how to create them! For this purpose Zircon supplies a factory object: Components. You can use the factory methods in this class to create a Builder for all built-in Components:

ButtonBuilder builder = Components.button();

You can use these builders to set up the Component you’re trying to create. All Components must have a Position, a Size and a ComponentRenderer. Luckily the builder classes come with pre-configured defaults, so in most cases you don’t have to bother with them. Let’s take a look at a more complex example:

AttachedComponent attachment = screen.addComponent(button()
        .withText("Click Me!") // 1
        .withAlignment(ComponentAlignments.alignmentWithin(screen, ComponentAlignment.CENTER)) // 2
        // 3
        .withDecorations(ComponentDecorations // 4
                .box(BoxType.SINGLE, "", RenderingMode.INTERACTIVE))
        .build());
attachment.onActivated(Functions.fromConsumer((event) -> { // 5
    attachment.detach(); // 6
}));

If you run this code you’ll see this:

Detaching Components

Let’s see what is happening here:

  1. Most Components support adding text to them. The Button is no different.
  2. We can align Components in multiple ways. In this example we align it within its parent, the screen.
  3. We don’t need to set a Size for a Component. In this case Zircon will automatically figure out the right size.
  4. Components support adding decorations to them. For this we can use the [ComponentDecorations] factory object. Here we create a box. More on decorations later.
  5. When we add a Component to a Container it gets attached and we get an AttachedComponent object back. This class adds functionality on top of Component which is only relevant for Components which are attached to a parent.
  6. Here we detach our Component which means that it will no longer be the part of the Component tree (it is removed from the Screen).

Aligning Components

Components can be aligned in multiple ways. In the example above we’ve seen how to align by creating an alignment object using ComponentAlignments. This class supports other options as well:

    fun alignmentWithin(tileGrid: TileGrid,
                        alignmentType: ComponentAlignment): AlignmentStrategy

    fun alignmentWithin(container: Container,
                        alignmentType: ComponentAlignment): AlignmentStrategy

    fun alignmentAround(component: Component,
                        alignmentType: ComponentAlignment): AlignmentStrategy

    fun positionalAlignment(x: Int, y: Int): AlignmentStrategy
  • alignmentWithin will align the Component within its parent. It can be centered, or aligned around the corners.
  • alignmentAround will produce alignments around the target Component.
  • positionalAlignment will set an absolute Position.

This example explores all the alignment options.

Another option is to use Containers which will auto-align their children: HBox and VBox. As their name suggests HBox will align their children horizontally, while VBox aligns vertically. These can also be nested to be able to create complex layouts easily:

HBox columns = hbox()
        .withSize(screen.getSize())
        .build();

Size columnSize = columns.getSize().withWidth(columns.getSize().getWidth() / 3);

for (int i = 0; i < 3; i++) {

    Size boxSize = columnSize.withHeight(columnSize.getHeight() / 3);

    VBox column = vbox().withSize(columnSize).build();
    columns.addComponents(column);

    for (int j = 0; j < 3; j++) {
        
        column.addComponent(panel()
                .withSize(boxSize)
                .withDecorations(box())
                .build());
    }
}

screen.addComponent(columns);

which will result in this:

Boxes

Decorating Components

In the previous examples we’ve used box decorations. But how do they work in practice? What happens when you create a Component is that the builder calculates the necessary space required for the content and adds the size of the decorations to it to arrive at the final Size for the Component.

You can see this in action in the Button example above. You can also set a Size by hand. In this case the size of all decorations are added together and the final contentSize will be the remaining space which is left. The contentPosition is calculated by adding up the space consumed by decorations on the top-left side of the Component. This means that a box() will give a contentPosition of (1, 1) because it offsets content with just 1 Tile on each side. A shadow() on the other hand won’t offset the content, and will take up space only on the bottom-right side:

ToggleButtons with decorations

Decorations can also be INTERACTIVE and NON_INTERACTIVE. In INTERACTIVE mode the decorations will inherit the current style of the Component they wrap. In NON_INTERACTIVE mode they will keep using the DEFAULT style (DISABLED if the Component is disabled):

Rendering Mode

Fragments

A Fragment is a Component combined with some view logic. You can add them to any ComponentContainer the same way as you do with Components. You can use this abstraction to create reusable parts for your UI without having to write a new Component from scratch:

class Sidebar implements Fragment {

    private VBox root;

    Sidebar(List<Button> buttons, Size size, String title) {
        root = vbox()
                .withSize(size)
                .withDecorations(box(BoxType.DOUBLE, title))
                .build();
        buttons.forEach(root::addComponent);
    }

    public Component getRoot() {
        return root;
    }
}

Button btn0 = button().withText("Press me.").build();
Button btn1 = button().withText("Click me.").build();

screen.addFragment(new Sidebar(Arrays.asList(btn0, btn1), Size.create(15, 10), "Sidebar"));

It will look like this:

Fragment Example

Using Views

Views are like Fragments, but for Screens. With this abstraction you can create reusable full-screen views:

ColorTheme theme = ColorThemes.arc();

TileGrid tileGrid = startTileGrid();

class InitialView extends BaseView {

    public Button dockOther = Components.button()
            .withText("Dock other")
            .withPosition(0, 2)
            .build();

    public InitialView(@NotNull TileGrid tileGrid, @NotNull ColorTheme theme) {
        super(tileGrid, theme);
        getScreen().addComponent(Components.header().withText("Initial view."));
        getScreen().addComponent(dockOther);
    }

    @Override
    public void onDock() {
        System.out.println("Docking Initial View.");
    }

    @Override
    public void onUndock() {
        System.out.println("Undocking Initial View.");
    }
}

class OtherView extends BaseView {

    public Button dockInitial = Components.button()
            .withText("Dock initial")
            .withPosition(12, 2)
            .build();

    public OtherView(@NotNull TileGrid tileGrid, @NotNull ColorTheme theme) {
        super(tileGrid, theme);
        getScreen().addComponent(Components.header().withText("Other view."));
        getScreen().addComponent(dockInitial);
    }

    @Override
    public void onDock() {
        System.out.println("Docking Other View.");
    }

    @Override
    public void onUndock() {
        System.out.println("Undocking Other View.");
    }
}

InitialView initial = new InitialView(tileGrid, theme);
OtherView other = new OtherView(tileGrid, theme);

initial.dockOther.onActivated(Functions.fromConsumer((event) -> other.dock()));

other.dockInitial.onActivated(Functions.fromConsumer((event) -> other.replaceWith(initial)));

initial.dock();

Running this will result in the following:

Views

Conclusion

This concludes the introductory material for Components. If you want to see more examples on how to work with Components you can navigate to the java component examples directory in the Zircon repo. Each example is inclusive and can be run as an application.

If you’re using Kotlin check out the Kotlin code examples for components here.

Do you have questions? Ask us on our Discord Server.

If you like what we do and want to support us consider becoming a Patron.