Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

classwhat it decidesthread
Guiwhich screen and which overlay exist, and what an absent screen meansRender thread
Screenone screen’s lifecycle, children, focus and narrationRender thread
AbstractWidgetthe final outer shape of a widget, and one inner hook per subclassRender thread
Layout over LayoutElementwhere widgets end up, re-arranged on most screens whenever the window changesRender thread
AbstractContainerScreena screen mirroring a server-side menu, and the slot geometryRender thread
MenuScreensMenuType to screen class — the registry ClientboundOpenScreenPacket looks a screen up inRender thread
Overlaytakes the screen’s place in the record pass, and suppresses its mouse and its typing — but not its key pressesRender thread
ScreenNarrationCollectorwhat has already been said, so it is not said twiceRender 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

routeexamples
entirely client-sideTitleScreen, PauseScreen, OptionsScreen, ChatScreen, social interactions, the survival and creative inventories
ClientboundOpenScreenPacketevery menu with a MenuType — chests, furnaces, anvils, and a chest boat
ClientboundMountScreenOpenPacketa horse’s or a nautilus’s own inventory
other packetsBookViewScreen, 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 is Hud, reached as Gui.hud. Gone with it: Minecraft.setScreen, Screen.render and renderBackground, AbstractContainerScreen.renderBg / renderLabels / renderSlot, AbstractWidget.renderWidget, ClickType (now ContainerInput) and MultiPlayerGameMode.handleInventoryMouseClick (now MultiPlayerGameMode.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.