Screen Operation API

Add controller compatibility to GUIs.

You can add explicit controller support to your UI if necessary with the various APIs in Controlify.

To make sure that your GUI classes don't depend on Controlify to load, all controller handling is done in a separate class, namely the ScreenProcessor, that is registered in Controlify's pre-init entrypoint.

Making a screen

Before we get to handling a Screen, first we need to make an example screen:

public class MyAmazingScreen extends Screen {
    public Button button1, button2;

    public MyAmazingScreen() {
        super(Component.literal("My Amazing Screen"))
    }
    
    @Override
    protected void init() {
        this.addDrawableWidget(button1 = Button.builder(..., btn -> doButton1Action()).build())
        this.addDrawableWidget(button2 = Button.builder(..., btn -> doButton2Action()).build())
    }
    
    public void doButton1Action() {
        System.out.println("This is a really cool button!");
    }
    
    public void doButton2Action() {
        System.out.println("This is also a really cool button!")
    }
}

In this basic example, there is a screen with two buttons that execute a function upon pressed. Even without a custom ScreenProcessor, this screen will work well out of the box.

However, we can enhance the experience using shortcuts using the GUI Abstract Actions 1 and 2 provided in the Controlify bindings.

To do this, we need to add create said ScreenProcessor.

Creating a ScreenProcessor

public class MyAmazingScreenProcessor extends ScreenProcessor<MyAmazingScreen> {
    public MyAmazingScreenProcessor(MyAmazingScreen screen) {
        super(screen);
    }
    
    @Override
    protected void handleButtons(Controller<?, ?> controller) {
        super.handleButtons(controller);
        // handle binds and other button actions here.
        // THIS IS ONLY CALLED WHEN VMOUSE IS OFF
    }
    
    @Override
    protected void handleVMouse(Controller<?, ?> controller, VMouseHandler vmouse) {
        // do what you want here, called every tick only if vmouse is enabled
    }
    
    @Override
    protected void handleTabNavigation(Controller<?, ?> controller) {
        super.handleTabNavigation(controller);
        // handles the vanilla tab widget found in the create new world screen
        // called regardless of vmouse state
    }
    
    @Override
    public void onWidgetRebuild() {
        super.onWidgetRebuild();
        // called after the Screen#init() method
    }
    
    @Override
    protected void render(Controller<?, ?> controller, GuiGraphics graphics, float tickDelta, Optional<VirtualMouseHandler> vmouse) {
        // called after the screen has rendered.
    }
    
    @Override
    public VirtualMouseBehaviour virtualMouseBehaviour() {
        // specifies how the vmouse should be used in this screen.
        // options are: DEFAULT, DISABLED, ENABLED, CURSOR_ONLY
        // DEFAULT allows the user to turn on/off, and is off by default.
        return VirtualMouseBehaviour.DEFAULT;
    }
}

If a custom processor is not provided for a screen, the parent class ScreenProcessor is just used. This provides the basic functionality for controller operation for screens. You can choose to completely override this functionality, or extend from it using super calls.

The above screen processor doesn't actually do anything, so let's add some functionality:

public class MyAmazingScreenProcessor extends ScreenProcessor<MyAmazingScreen> {
    public MyAmazingScreenProcessor(MyAmazingScreen screen) {
        super(screen);
    }
    
    @Override
    protected void handleButtons(Controller<?, ?> controller) {
        super.handleButtons(controller);
        
        if (controller.bindings().GUI_ABSTRACT_ACTION_1.justReleased()) {
            // remember, we made this button public.
            // screen is a field in ScreenProcessor
            screen.button1.onPress();
        }
        
        if (controller.bindings().GUI_ABSTRACT_ACTION_2.justReleased()) {
            screen.button2.onPress();
        }
    }
}

In the above example, I removed most of the methods and just left handleButtons, then added a simple if statement to check if the bindings were pressed, then told the button to press. You could even use your own bindings using BindingSupplier that you learnt about in the other wiki page.

Attaching the ScreenProcessor

There are two ways to attach a processor; you can do it through a mixin, or the registry.

Using the registry

Inside of the pre-init entrypoint, add the following code...

@Override
public void onControlifyPreInit(ControlifyApi controlify) {
    // ...
    ScreenProcessorProvider.REGISTRY.register(
        MyAmazingScreen.class,
        MyAmazingScreenProcessor::new
    )
}

Using Mixin

You can make your screen implement the ScreenProcessorProvider interface which does as it says on the tin.

@Mixin(MyAmazingScreen.class)
public class MyAmazingScreenMixin implements ScreenProcessorProvider {
    @Unique private final ScreenProcessor<?> processor =
            new MyAmazingScreenProcessor((MyAmazingScreen) (Object) this);
            
    @Override
    public ScreenProcessor<?> screenProcessor() {
        return this.processor;
    }
}

Make sure to not initialise the processor inside the getter function, as it could be ran multiple times per tick.

It is not inherently bad to mixin into your own classes, so don't feel that this in an 'improper' way. However using the registry is probably better. This method is useful when adding processors to vanilla screens, where you have easier access to fields and functions with @Shadow that you can pass into your processor.

Using ComponentProcessor

The screen processors work great for handling global actions within the screen, but don't provide any way to control the interaction behaviour of individual GUI components. This is where component processors come in.

For the example, we will create a processor for the vanilla button widget. For all intensive purposes, all we need to know about AbstractButton is that it has an abstract onPress() method:

public abstract class AbstractButton implements GuiEventListener {
    public abstract void onPress();
}

Now we create our component processor:

public class AbstractButtonProcessor implements ComponentProcessor {
    @Override
    public boolean overrideControllerNavigation(ScreenProcessor<?> screen, Controller<?, ?> controller) {
        return false;
    }
    
    @Override
    public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
        return false;
    }
    
    @Override
    public void onFocusGained(ScreenProcessor<?> screen, Controller<?, ?> controller) {
        
    }
}

Notice how both the 'override' methods return boolean. The return value dictates whether or not the corresponding screen processor methods are ran. This processor doesn't do much, so let's add the functionality to run the onPress() method.

public class AbstractButtonProcessor implements ComponentProcessor {
    private final AbstractButton button;
    
    public AbstractButtonProcessor(AbstractButton button) {
        this.button = button;
    }

    @Override
    public boolean overrideControllerButtons(ScreenProcessor<?> screen, Controller<?, ?> controller) {
        if (controller.bindings().GUI_PRESS.justReleased()) {
            button.onPress();
            return true; // override - now ScreenProcessor#handleButtons will not run
        }
        
        return false; // nothing happens! screen processor runs as usual
    }
}

Here we added a constructor so we have access to the button, removed the unneeded overrides and added yet another quick binding check to press the button. Again, this can be switched out for your own binding that you created in the other wiki page.

The use case of a component processor shown above will actually not behave any differently to if it wasn't there. This is because if there is no component processor, GUI_PRESS will just emulate the enter key, which buttons already handle for. However, it is always best to have explicit compatibility to not rely on emulation.

Attaching a ComponentProcessor

You attach a component processor no differently from how you attach a screen processor. You have two options: the registry, or a mixin.

Using the registry

Inside of the pre-init entrypoint, add the following code...

@Override
public void onControlifyPreInit(ControlifyApi controlify) {
    // ...
    ComponentProcessorProvider.REGISTRY.register(
        AbstractButtton.class,
        AbstractButtonProcessor::new
    )
}

Using Mixin

You can make your component implement the ComponentProcessorProvider interface which does as it says on the tin.

@Mixin(AbstractButton.class)
public class AbstractButtonMixin implements ComponentProcessorProvider {
    @Unique private final ComponentProcessor processor =
            new AbstractButtonProcessor((AbstractButton) (Object) this);
            
    @Override
    public ComponentProcessor componentProcessor() {
        return this.processor;
    }
}

Make sure to not initialise the processor inside the getter function, as it could be ran multiple times per tick.

Using button guides

The screen now fully works with a controller, but it is not clear what buttons press what on your screen. This is where the button guide API comes in; you can easily add binding previews to buttons.

To add a button guide, we need to modify our screen processor and add an override for the onWidgetRebuild method. Since this runs after our buttons have initialised.

public class MyAmazingScreenProcessor extends ScreenProcessor<MyAmazingScreen> {
    // ...
    
    @Override
    public void onWidgetRebuild() {
        super.onWidgetRebuild();
        
        ButtonGuideApi.addGuideToButtonBuiltin(
                screen.button1, 
                bindings -> bindings.GUI_ABSTRACT_ACTION_1,
                ButtonRenderPosition.TEXT, // renders beside the text
                ButtonRenderPredicate.ALWAYS
        );
        ButtonGuideApi.addGuideToButtonBuiltin(
                screen.button2, 
                bindings -> bindings.GUI_ABSTRACT_ACTION_2,
                ButtonRenderPosition.TEXT, // renders beside the text
                ButtonRenderPredicate.ALWAYS
        );
    }
}

If you want to use a custom binding, remove the 'builtin' suffix and replace the lambda with your BindingSupplier.

More examples

If you want to see some more advanced examples, have a look at the Controlify source in your IDE. The following packages will be very helpful:

  • dev.isxander.controlify.screenop.compat

  • dev.isxander.controlify.mixins.feature.screenop.vanilla

  • dev.isxander.controlify.mixins.compat

Last updated