A screen is a graphical user interface that extends Screen
, allowing the user to interact and fulfill some functionalities. One example of a screen is a custom config screen of your mod. Screens only exist in the client, so you can annotate the relavant classes with @Environment(EnvType.CLIENT)
.
You may use mixins to add into an existing screen a button that goes to your screen. But in many cases, we can implement the ModMenuApi
of Mod Menu mod, and make it possible to access the screen via the config button in the Mod Menu screen. This article does document how to implement ModMenuApi
.
A screen should have several “widgets”, which refer to elements in the screen. We add widgets in the init
method.
@Environment(EnvType.CLIENT) public class TutorialScreen extends Screen { protected TutorialScreen() { // The parameter is the title of the screen, // which will be narrated when you enter the screen. super(Text.literal("My tutorial screen")); } public ButtonWidget button1; public ButtonWidget button2; @Override protected void init() { button1 = ButtonWidget.builder(Text.literal("Button 1"), button -> { System.out.println("You clicked button1!"); }) .dimensions(width / 2 - 205, 20, 200, 20) .tooltip(Tooltip.of(Text.literal("Tooltip of button1"))) .build(); button2 = ButtonWidget.builder(Text.literal("Button 2"), button -> { System.out.println("You clicked button2!"); }) .dimensions(width / 2 + 5, 20, 200, 20) .tooltip(Tooltip.of(Text.literal("Tooltip of button2"))) .build(); addDrawableChild(button1); addDrawableChild(button2); } }
The init
method is called then:
You must use add the elements to the screen via using addDrawable
, addSelectableChild
or addDrawableChild
. The difference is:
addDrawable
: The element will be rendered, but you cannot select it, either by using mouse or keyboard.addSelectableChild
: You can select and interact it, but it will not be rendered.addDrawableChild
: The element will be both rendered and interactable, which is the most common case.
In the ButtonWidget.builder(…).builder()
, you can specify the size and position of the button by using size
and position
respectively, or directly using dimensions
. The tooltip
specifies the tooltip, which will be rendered and narrated when your mouse hovers on, or use Tab to focus on it. Tooltip.of
takes two arguments, the first to be shown, and the second (optional) to be narrated.
For versions before 1.19.3, ButtonWidget.builder(…)
does not exist. In that case, please directly invoke the constructor of ButtonWidget
.
Some users may instantiate the widgets in the constructor, or the initialization of class. For example, they may write the code like this:
public ButtonWidget button1 = ButtonWidget.builder(...).build(); public ButtonWidget button2 = ButtonWidget.builder(...).build(); @Override protected void init() { addDrawableChild(button1); addDrawableChild(button2); }
This is also OK. Its advantage is, if the widgets have some several states (such as the current selections of CyclingWidget
, or typed text in the TextFieldWidget
), they will not be reset when you resize the screen, because they will not be created again. However, when resizing, the width
and height
of screen are changed, but the positions and sizes of elements will not update. Therefore, in this case, you have to update the sizes or positions in the init
method.
@Override protected void init() { button1.setPosition(width / 2 - 205, 20); button2.setPosition(width / 2 + 5, 20); addDrawableChild(button1); addDrawableChild(button2) }
After adding amounts of elements, all of them can render and be selected. Some people don't care about the order they are added, because all of the widgets are rendered at the sametime. However, if you select widgets by pressing the “Tab” key, you may find they are focused in a messy order. Therefore, please ensure that the widgets are added in a correct order, which the behaviour of “Tab” key depends on.
I accessed the screen via another screen, such as the Mod Menu screen, but when I press “Esc” to go back, it just jumped to the main screen, not the previous screen, why?
This is because you did not specify a parent screen, and the close
method just directly jumps to the main screen.
Add a parent
as a parameter and field, and use it in the close
method:
private final Screen parent; protected TutorialScreen(Screen parent) { super(Text.literal("My tutorial screen")); this.parent = parent; } @Override public void close() { client.setScreen(parent); }
By default, when narration is enabled, the screen title and information of the element you hover or focus on will be narrated. If the screen requires extra narrations (for example it has some texts rendered but not added as a widget), you can override addScreenNarrations
or addElementNarrations
. The methods take a NarrationBuilder
, in which you can use add
method to add narration messages. The narration messages are divided into the following parts (NarrationPart
):
tooltip
method when you create the ButtonWidget
in the code above. The tooltip is narrated in this part.
Besides the narration of the screen, you can also customize the narration of the element, by overriding appendNarrations
method of the class of that element. The element is narrated after the narration of the screen.
In the method of appending narrations, using narrationBuilder.nextMessage()
can append narrations after the current narrations, instead of replacing existing part of the narration.
In some cases, you want repetitive narrations, instead of narrating only once. For example, when loading a level, the percentage of loading is narrated repetitively, telling the user the current loading status. You can call narrateScreenIfNarrationEnabled
in the render
or tick
method. For more details, you may refer to sources of LevelLoadingScreen
.
In render
method, you can invoke methods like textRenderer.draw
, drawTextWithShadow
or drawCenteredTextWithShadow
to render a text on the screen.
// For versions 1.20 below @Override public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { super.render(matrices, mouseX, mouseY, delta); drawCenteredTextWithShadow(matrices, textRenderer, Text.literal("You must see me"), width / 2, height / 2, 0xffffff); } // For versions 1.20 and after @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { super.render(context, mouseX, mouseY, delta); context.drawCenteredTextWithShadow(textRenderer, Text.literal("You must see me"), width / 2, height / 2, 0xffffff); }
If you're concerned that the text can be pretty long and may exceed the screen limit, you can use MultilineText
so it can be wrapped smartly.
final MultilineText multilineText = MultilineText.create(textRenderer, Text.literal("The text is pretty long ".repeat(20)), width - 20); // For versions 1.20 below multilineText.drawWithShadow(matrices, 10, height / 2, 16, 0xffffff); // For versions 1.20 and after multilineText.drawWithShadow(context, 10, height / 2, 16, 0xffffff);
Another alterative is using TextWidget
or MultilineTextWidget
. They are by default not selectable or narratable because their active
field is false
.
The screen does not support scrolling, but you can add widgets that supports scrolling. EntryListWidget
is a class for widgets that contains multiple entries and supports scrolling. However, instead of directly extending it, it is more suitable to extend AlwaysSelectedEntryListWidget
or ElementListWidget
, which already implemented navigation and narration. The difference is:
AlwaysSelectedEntryListWidget
refers to a widget in which you can select a row. In widgets that extends the class, you usually select one entry in the list. Some vanilla examples are biome selection screen in the buffet (single biome) world option, and the language selection screen.ElementListWidget
refers to a widget where each row has child elements. In widgets that extends this class, you select and interacts the elements in the rows. Like a Screen
, the ElementListWidget.Entry
should have zero, one, or more child elements.After finishing your screen, in order to avoid potential issues, please check: