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.2 · 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. 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 only registry of screens in the gameRender thread
Overlaysuppresses the screen’s record pass, 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

flowchart TD
    Gui["Gui — the manager, once per game"]
    Screen["Screen — zero or one"]
    Overlay["Overlay — zero or one, and it wins"]
    Hud["Hud — reached as Gui.hud"]
    Toasts["ToastManager, ChatListener, SplashManager"]
    Children["Screen.children — GuiEventListener, gets input"]
    Rend["Screen.renderables — Renderable, gets recorded"]
    Narr["Screen.narratables — NarratableEntry, gets described"]
    Widget["AbstractWidget — usually in all three lists at once"]
    Layout["Layout over LayoutElement — arranges, then forgets"]
    ACS["AbstractContainerScreen — a Screen with a menu behind it"]
    Menu["AbstractContainerMenu — shared with the server"]
    Gui --> Screen
    Gui --> Overlay
    Gui --> Hud
    Gui --> Toasts
    Screen --> Children
    Screen --> Rend
    Screen --> Narr
    Children --> Widget
    Rend --> Widget
    Narr --> Widget
    Layout --> Widget
    Screen --> ACS
    ACS --> Menu

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, Gui.openChatScreen, Gui.canInterruptScreen, Gui.buildInitialScreens and Gui.setClientLevelTeardownInProgress.

Two 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 what stops the integrated server, and it asks the screen. Screen.isPauseScreen defaults to true and AbstractContainerScreen overrides it to false — which is the whole reason the options screen pauses a singleplayer world and a chest does not. An overlay pauses by default too.

And an overlay does not stack on a screen: in the record pass it replaces it. Nothing draws both. LoadingOverlay is the only implementation of Overlay in the game.

The lifecycle, and what is final

Screen.init is final, and a resize goes through Screen.resize to Screen.repositionElements. The default Screen.repositionElements rebuilds every widget through Screen.rebuildWidgets, which does re-enter the overridable Screen.init 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.

The framework fixes the outer shape everywhere and hands the subclass one inner hook. Screen.init, 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 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 MultiPlayerGameMode

    KH->>KH: keyPress — no screen is open, so the mapping records a click
    Note over MC: next client tick
    MC->>MC: handleKeybinds — only with no screen and no overlay
    MC->>MPGM: isServerControlledInventory? false for a player on foot
    MC->>MC: Tutorial.onOpenInventory
    MC->>Gui: setScreen(new InventoryScreen(player))
    Gui->>Gui: MouseHandler.releaseMouse, then KeyMapping.releaseAll — both before init
    Gui->>InvS: removed on the old screen, then added, then Screen.init
    InvS->>InvS: init — creative? replace myself with CreativeModeInventoryScreen
    Note over Gui: next frame, record
    Gui->>InvS: extractRenderStateWithTooltipAndSubtitles
    InvS->>InvS: extractBackground — in-game UI, so a gradient, no blur, no panorama
    InvS->>InvS: extractContents, then labels, slots, the hovered highlight, the carried item

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.

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-sidetitle, pause, options, chat, advancements, 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 packetsthe book viewer, the sign editor, the death screen, the win screen, the demo popup, the level-loading screen, dialogs

Three entities implement HasCustomInventoryScreen, and they do not agree: two use the mount packet and one falls back to the ordinary menu packet.

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 data fix or a save.

Narration, finally, is mostly timed rather than immediate — mostly, because Screen.init narrates the new screen at once before arming anything. Thereafter Screen.handleDelayedNarration fires from Gui.update once two clocks have passed — one delay after a mouse move, a shorter one after a keyboard action, and a two-second suppression after a screen is built — and then 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: Screen.render, renderBackground and renderDirtBackground; AbstractContainerScreen.renderBg / renderLabels / renderSlot; AbstractWidget.renderWidget; ClickType (now ContainerInput); MultiPlayerGameMode.handleInventoryMouseClick (now MultiPlayerGameMode.handleContainerInput); and Minecraft.setScreen. 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. 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 the second one.


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.