GUI and screens
Verified against Minecraft 26.3 · Part X · pressing E: a screen the server is not told about until you close it, opened onto a menu that was built when you spawned.
Press E in survival and no packet is sent, no packet is received, and nothing
on the server changes. Player.inventoryMenu has existed since the player
object was constructed, it has no MenuType at all, and MenuScreens
could therefore never build an InventoryScreen from a packet even if one
arrived. The opening is entirely a client-side event. Press E again and the
symmetry breaks: LocalPlayer.closeContainer sends
ServerboundContainerClosePacket, and the server empties your 2×2 crafting
grid on the way out, returning or dropping what was in it. The screen the server is never told about is one the
server is told about exactly once, at the end.
The menu underneath it is not symmetric in the same way, and the qualification
matters: InventoryMenu is constructed on both sides, and its crafting
result is recomputed only on the server — so the client is rendering a result
it did not compute, in a screen the server does not know is open.
This page is what a screen is: the manager that holds one, the lifecycle it
runs through, the widget and layout families it is built from, and the four
routes by which one comes to exist. How its contents become pixels is the
GUI render tree; how a Component becomes glyphs is
text and fonts.
The cast
| class | what it decides | thread |
|---|---|---|
Gui | which screen and which overlay exist, and what an absent screen means | Render thread |
Screen | one screen’s lifecycle, children, focus and narration | Render thread |
AbstractWidget | the final outer shape of a widget, and one inner hook per subclass | Render thread |
Layout over LayoutElement | where widgets end up, re-arranged on most screens whenever the window changes | Render thread |
AbstractContainerScreen | a screen mirroring a server-side menu, and the slot geometry | Render thread |
MenuScreens | MenuType to screen class — the registry ClientboundOpenScreenPacket looks a screen up in | Render thread |
Overlay | takes the screen’s place in the record pass, and suppresses its mouse and its typing — but not its key presses | Render thread |
ScreenNarrationCollector | what has already been said, so it is not said twice | Render thread |
The objects, and what contains what
classDiagram
class Gui {
one per game
Screen screen
Overlay overlay
Hud hud
}
class Screen {
List~GuiEventListener~ children
List~Renderable~ renderables
List~NarratableEntry~ narratables
}
class AbstractWidget {
usually in all three lists
}
class AbstractContainerScreen {
AbstractContainerMenu menu
}
class Layout {
arranges; most screens re-arrange it on resize
}
Gui *-- Screen
Gui *-- Overlay
Gui *-- Hud
Screen *-- AbstractWidget
Screen <|-- AbstractContainerScreen
AbstractContainerScreen *-- AbstractContainerMenu
Layout ..> AbstractWidget : positions
A diamond is containment and a hollow arrowhead is inheritance, which the
flowchart this replaced could not tell apart: AbstractContainerScreen is
a Screen, and everything else here holds what it points at. Layout is the dashed one because it arranges widgets the screen owns.
The three lists on Screen are the shape worth remembering: a widget added
with Screen.addRenderableWidget joins all three, and the sibling add methods
exist precisely so that something can be in one or two of them and not the
rest. A Tooltip is in none of them — it is held by a WidgetTooltipHolder
— and MultiLineLabel is an interface rather than a widget at all.
Gui, which is not the HUD
Gui owns Gui.screen and Gui.overlay — set through Gui.setScreen and
Gui.setOverlay — plus Gui.hud, Gui.toastManager, Gui.chatListener,
Gui.splashManager and the reference to the frame’s render state. It has
three cadences, not two: Gui.tick once per client tick, Gui.update
once per frame — which advances toasts and fires delayed narration — and
Gui.extractRenderState once per frame in the record pass. The rest of its
surface is Gui.isPausing, Gui.handleKeybinds (the share of the drain Minecraft.handleKeybinds hands it), Gui.openChatScreen,
Gui.canInterruptScreen, Gui.buildInitialScreens and
Gui.setClientLevelTeardownInProgress.
Three of its behaviours are the sort of thing a reader assumes and gets wrong.
Gui.setScreen with a null screen does not mean “close the screen”. It means “decide
what should be up instead”. With no level it substitutes the title screen;
with a dead player it substitutes the death screen, or respawns; otherwise it
restores the chat screen if one was saved. During a level teardown it throws,
rather than return you to a world that is being dismantled.
Gui.isPausing is the screen’s vote on whether the game stops, and it is
one of three conjuncts: the client
loop
owns the pause itself and the other two, and the vote is cast by Screen.isPauseScreen, which defaults to true and which AbstractContainerScreen.isPauseScreen answers false. An overlay pauses by
default too, which the loop page does not say, and is why a resource reload
stops a singleplayer world as surely as the options screen does.
An overlay does not stack on a screen: in Gui’s record pass it replaces it. Only LoadingOverlay itself records the screen beneath it, while it fades in and out, and the input rules are split — an overlay
suppresses the screen’s mouse and its typing, but not its key presses.
LoadingOverlay is the only implementation of Overlay in the game.
The lifecycle, and what is final
There are two methods called Screen.init and the difference between them is
the lifecycle. The one that takes a width and a height is final — the
framework’s entry point, which nothing overrides — and the no-argument one is the hook a screen implements to build its widgets. Wherever this page says
the Screen.init hook it means the second.
A resize goes through Screen.resize to Screen.repositionElements. The
default Screen.repositionElements rebuilds every widget through
Screen.rebuildWidgets, which re-enters the hook — so on a plain screen
everything really is rebuilt. Forty-one screens override
Screen.repositionElements instead and keep their widgets, most of them just
re-arranging their Layout. “Everything is rebuilt on resize” is true of a
plain screen and false of most interesting ones.
The rest of the lifecycle is Screen.added, Screen.tick, Screen.removed
and Screen.onClose, and the record entry point is the final
Screen.extractRenderStateWithTooltipAndSubtitles.
That pairing is the framework’s whole style: fix the outer shape, hand the
subclass one inner hook. Screen.init’s two-argument form,
Screen.extractRenderStateWithTooltipAndSubtitles,
AbstractWidget.extractRenderState, AbstractWidget.updateNarration,
AbstractButton.extractWidgetRenderState and AbstractContainerScreen.tick
are all final; AbstractWidget.extractWidgetRenderState and
AbstractButton.extractContents are the hooks they leave open. The widget
family under them is Button, EditBox, Checkbox, CycleButton,
AbstractScrollArea and the selection lists over it —
AbstractSelectionList, ObjectSelectionList,
ContainerObjectSelectionList and OptionsList.
Layout is Layout over LayoutElement: LinearLayout, GridLayout,
FrameLayout, EqualSpacingLayout, HeaderAndFooterLayout and
SpacerElement, configured by LayoutSettings and resolved by
Layout.arrangeElements and Layout.visitWidgets. Input arrives as the
client/input records — KeyEvent, MouseButtonEvent and their siblings,
which input and keybinds owns — through
GuiEventListener and ContainerEventHandler; focus is a ComponentPath moved by a
FocusNavigationEvent, ordered by TabOrderedElement.getTabOrderGroup; and
geometry is ScreenRectangle, ScreenPosition, ScreenAxis and
ScreenDirection.
One consequence of doing all this in a record pass:
AbstractWidget.extractRenderState computes hovered as “inside my
rectangle and inside the current scissor”, so a widget scrolled out of a
list does not light up.
Pressing E
sequenceDiagram
participant KH as KeyboardHandler
participant MC as Minecraft
participant Gui as Gui
participant InvS as InventoryScreen
participant MPGM as MultiPlayer<br/>GameMode
rect rgba(0, 0, 0, 0.04)
Note over KH,MPGM: the key press, then the tick that spends it
KH->>KH: keyPress — no screen open, so the mapping records a click
MC->>MC: handleKeybinds, with no screen and no overlay
MC->>MPGM: isServerControlledInventory — false on foot
MC->>Gui: setScreen — a new InventoryScreen, and the input housekeeping
Gui->>InvS: added, then init
InvS->>InvS: init — creative? replace myself with<br/>CreativeModeInventoryScreen
end
rect rgba(0, 0, 0, 0.04)
Note over KH,MPGM: the next frame, recording
Gui->>InvS: extractRenderState<br/>WithTooltipAndSubtitles
InvS->>InvS: extractBackground — a gradient, no blur, no panorama
InvS->>InvS: extractContents, then labels, slots, highlight, carried item
end
Two bands: everything that makes the screen exist happens in the tick that spends the key, and nothing is recorded until the frame that follows it. Gui.setScreen’s housekeeping — the mouse ungrabbed, every mapping released, sneak with them, held or toggled — runs before Screen.init.
The busiest screen in the game is AbstractContainerScreen, and its record
pass is worth following once: AbstractContainerScreen.extractContents draws
the widget list, translates to the container origin, and runs
AbstractContainerScreen.extractLabels,
AbstractContainerScreen.extractSlots and the two slot-highlight passes;
then AbstractContainerScreen.extractCarriedItem, then
AbstractContainerScreen.extractTooltip. It holds
AbstractContainerScreen.menu, AbstractContainerScreen.leftPos,
AbstractContainerScreen.topPos, AbstractContainerScreen.hoveredSlot and
the quick-craft state, and a click goes
AbstractContainerScreen.slotClicked to
MultiPlayerGameMode.handleContainerInput — see containers and
menus,
which also owns the fact this page’s second paragraph turns on: the client
never computes a crafting result.
The final AbstractContainerScreen.tick closes the container when the player
is dead or removed. The client notices first.
Who opens a screen
| route | examples |
|---|---|
| entirely client-side | TitleScreen, PauseScreen, OptionsScreen, ChatScreen, social interactions, the survival and creative inventories |
ClientboundOpenScreenPacket | every menu with a MenuType — chests, furnaces, anvils, and a chest boat |
ClientboundMountScreenOpenPacket | a horse’s or a nautilus’s own inventory |
| other packets | BookViewScreen, AbstractSignEditScreen, DeathScreen, WinScreen, the demo popup, LevelLoadingScreen, dialogs |
Three entities implement HasCustomInventoryScreen and they do not agree
about which packet to use: AbstractHorse and AbstractNautilus send the
mount packet, while AbstractChestBoat opens an ordinary menu like any other
container.
Every screen in that table is an example, and the book keeps them that
way. There are two hundred-odd classes in client/gui/screens and its
eighteen sub-packages — the world-creation flow, the pack picker, the report
screens, the friends and social lists, the recipe book, a class per container
— and they are the single largest thing in Part X by line count. Every one of
them is this section’s four routes, the lifecycle above and the widget and
layout families above, and nothing else; the book explains the pattern once,
here, and names a screen only where some other page’s scenario walks into
it. The
exception it does not cover is player reporting, declined for its own
reasons in what this book
skips.
The screens you see first are a chain rather than a screen.
Gui.buildInitialScreens composes accessibility onboarding, ban notices, a
forced name change and a banned-skin notice ahead of the title screen or a
quick-play launch. And Minecraft.setScreenAndShow sets a screen and then
renders one frame on the spot — synchronously — which is how progress appears during blocking main-thread work such as a world load; a backup or a file fix shows its screen the same way and then runs on the worker pool.
Narration, finally, is mostly timed rather than immediate — mostly, because
Screen.init narrates the new screen at once before arming anything. What
does the speaking is GameNarrator, a thin wrapper over Mojang’s text2speech
library with a four-valued NarratorStatus option in front of it and two
tempers: the queued methods, which yield to whatever is already being said,
and GameNarrator.saySystemNow, which interrupts.
Thereafter Screen.handleDelayedNarration fires from Gui.update once two
clocks have both run out: a next narration time, armed longer after a mouse
move than after a keyboard action, and a suppression time, armed by the two-argument Screen.init on every Gui.setScreen — two seconds ahead, or, when one screen replaces another after Tab or arrow-key navigation, until the next key press or click. Three intervals, two clocks. When both have
passed it picks a single widget to narrate, by tab-order group and
priority.
For a 1.21-era reader. Minecraft.screen is gone: the current screen belongs to
Gui, which is now the screen-and-overlay manager rather than the HUD — the HUD isHud, reached asGui.hud. Gone with it: Minecraft.setScreen, Screen.render and renderBackground, AbstractContainerScreen.renderBg / renderLabels / renderSlot, AbstractWidget.renderWidget, ClickType (nowContainerInput) and MultiPlayerGameMode.handleInventoryMouseClick (nowMultiPlayerGameMode.handleContainerInput). A screen no longer draws — it records.
Where to look
Gui.setScreen — the substitution tree, and the input housekeeping at both
ends of a screen’s life that input and
keybinds
explains. Screen.init and Screen.resize for the lifecycle,
Screen.extractRenderStateWithTooltipAndSubtitles for the record pass, and
Gui.extractRenderState for the frame’s contributor order.
AbstractContainerScreen.extractContents for the busiest screen in the game,
and MenuScreens for the screen registry the menu types use — DialogScreens is another.
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.