Aller au contenu principal

Layout Types

Layouts are the building blocks of a menu. Each one is declared in the layoutPool as {"case": "<type>", "value": {...}} and referenced by id.

LayoutUse it forNests children?
simpleStatic grids: borders, buttons, fixed content
compositeStacking several layouts (background + content)✔ by id
scrollableContent bigger than the window (lists, maps)✔ one inner
paginatedDiscrete pages with prev/next buttons
frameIndependent zones (sidebar + main panel)✔ per frame
flexAuto-arranged rows (centered button bars)
storagePersistent item slots (deposits, crafting inputs)
book / merchantVanilla book pages / trading UI

Positions are virtual coordinates: x = column, y = row, starting at (0,0) top-left. The viewport shows 9 columns × the inventory's rows; anything outside is clipped (and reachable by scrolling, if wrapped in a scrollable).

simple — Static grid

A flat list of items at fixed positions. The workhorse layout.

{ "case": "simple", "value": {
"id": "border",
"items": [
{ "x": 0, "y": 0, "count": 9, "direction": "right",
"item": { "material": "GRAY_STAINED_GLASS_PANE" }, "displayName": " " }
]
}}

Repetition fields (avoid copy-pasting the same pane 9 times):

FieldMeaning
countHow many copies to place
directionright, left, down, uprequired to repeat
gapStep between copies (default 1 = adjacent; 2 = every other slot)
repeatYRepeat the whole line N times on the other axis
attention

direction is required for repetition. With no direction, count, gap and repeatY are ignored and you get a single slot at (x, y). A row of 9 panes needs both "count": 9 and "direction": "right"count on its own does nothing. This is the #1 reason a border "doesn't spread".

A full border needs four items — one per edge — because a straight repeat can't turn corners. For a 6-row menu:

Edgexydirectioncount
Top00right9
Bottom05right9
Left01down4
Right81down4

For an R-row menu: the bottom row is y = R-1, and the left/right edges use count = R-2 — they fill only the rows between top and bottom (the top/bottom full rows already cover the four corners).

composite — Stack layers

Combines several layouts by id. Later children render on top of earlier ones — order is your z-order.

{ "case": "composite", "value": {
"id": "main",
"children": ["background", "content", "nav_buttons"]
}}

Use for: the root of almost every menu (background + content + navigation).

scrollable — Viewport over large content

Wraps one inner layout (referenced by innerId) whose virtual size can exceed the window. The player scrolls the viewport across it.

{ "case": "scrollable", "value": {
"id": "list_scroll",
"innerId": "list_content",
"virtualWidth": 9,
"virtualHeight": 20,
"buttons": [
{ "direction": "UP", "step": 1,
"item": { "x": 8, "y": 0, "item": { "material": "ARROW" }, "displayName": "<white>▲" } },
{ "direction": "DOWN", "step": 1,
"item": { "x": 8, "y": 2, "item": { "material": "ARROW" }, "displayName": "<white>▼" } }
]
}}
attention

virtualHeight is the total content height (20 rows of content → 20), not the visible height. Navigation buttons only appear when the content actually overflows.

Key points:

  • Buttons are yours to define via buttons (direction: UP/DOWN/LEFT/RIGHT, step = slots per click). There are no automatic default arrows.
  • Buttons are anchored to the viewport — they stay in place while content scrolls behind them.
  • Scrolling can also come from interactions: gui:scroll 0 1 list_scroll on any slot, or mouse-wheel/WASD bindings.
  • Each scrollable has its own scroll state in the session, tracked by its id — you can have several independent scroll areas in one menu.

paginated — Discrete pages

Splits a flat item list into pages of itemsPerPage, laid out on the given slots indices, with optional prev/next buttons.

{ "case": "paginated", "value": {
"id": "shop_pages",
"itemsPerPage": 21,
"slots": [10,11,12,13,14,15,16, 19,20,21,22,23,24,25, 28,29,30,31,32,33,34],
"items": [ /* one entry per product; they flow page by page */ ],
"previousPage": { "item": { "x": 0, "y": 5, "item": { "material": "ARROW" }, "displayName": "<white>◀ Previous" } },
"nextPage": { "item": { "x": 8, "y": 5, "item": { "material": "ARROW" }, "displayName": "<white>Next ▶" } }
}}
  • slots are physical indices (index = y*9 + x); items fill them in order.
  • Prev/next buttons appear only when that direction exists (no "previous" on page 1).
  • Page state is per-player and per-layout-id.

Scrollable vs paginated? Scrolling feels continuous (maps, long lists); pagination is better for discrete catalogs (shops) and shows a stable grid per page.

frame — Independent zones

Divides the window into rectangles, each rendering its own layout with its own coordinate space. (0,0) inside a frame is the frame's top-left corner.

{ "case": "frame", "value": {
"id": "split_view",
"frames": [
{ "id": "sidebar", "x": 0, "y": 0, "width": 2, "height": 6, "layoutId": "sidebar_content" },
{ "id": "panel", "x": 3, "y": 0, "width": 6, "height": 6, "layoutId": "panel_content" }
]
}}

Use for: sidebar + main panel UIs, tab bars, dashboards. Combine with scrollable inside a frame for a scrolling sidebar next to static content.

flex — Auto-arranged rows

Positions items automatically with CSS-Flexbox-like rules — no manual coordinates.

{ "case": "flex", "value": {
"id": "action_bar",
"justifyContent": "CENTER",
"alignItems": "END",
"wrap": true,
"items": [ /* buttons — x/y are ignored, order matters */ ]
}}
  • justifyContent: START, CENTER, END, SPACE_BETWEEN — horizontal distribution.
  • alignItems: START, CENTER, END — vertical placement of the row block.
  • wrap: overflowing items continue on the next row.

Use for: button bars that stay centered regardless of how many buttons you add.

Addressable views

Views are a menu-level feature rather than a layout type. Use a frame layout as the shell and set a frame's layoutId to @view. Each MenuViewData then supplies the layout for that frame, either explicitly through frames or through the <viewId>_<frameId> naming convention. This keeps tabs, borders, and navigation shared while only the active content changes.

Slots tagged with buttonPrefix: "view:" and buttonType: "<viewId>" become view tabs. The GUI extension wires their click to gui:view <viewId>, hides tabs gated by criteria or permissions, and applies the configured active style. Parent views provide breadcrumbs and keep their tab active for nested screens.

See the complete Addressable Views guide for inheritance, history, and a full YAML example.

storage — Persistent item slots

Real slots the player can put items into; contents persist in a gui_storage artifact, per player or per group. See the gui_storage entry for click behaviors, accumulation mode and placeholders ({stored_name}, {stored_amount}, {stored_max}).

{ "case": "storage", "value": {
"id": "deposit",
"entry": "<gui_storage artifact id>",
"groupKey": "island_%island_id%",
"slots": [
{ "x": 3, "y": 1, "maxStack": 64,
"placeholder": { "material": "LIGHT_GRAY_STAINED_GLASS_PANE" } },
{ "x": 5, "y": 1, "temporary": true,
"requiredItem": { "material": "DIAMOND" }, "requiredAmount": 8,
"onReachRequired": ["<trigger entry id>"] }
]
}}
  • temporary: true — contents are returned/cleared when the menu closes (crafting inputs).
  • requiredItem + requiredAmount + onReachRequired — fire triggers when a quota is met (quests, crafting).
  • groupKey supports placeholders → shared storages (island chests, team banks).

book — Vanilla book UI

A special top-level layout that opens a written book instead of a chest window, with MiniMessage-formatted pages.

It cannot be nested inside another layout.

merchant — Vanilla trading UI

A special top-level layout that opens a villager trading screen with configured trades (item costs and results). Input items are safely returned when the menu closes.

It cannot be nested inside another layout.

API-only layouts & widgets

Extensions building menus in Kotlin (via MenuBuilder) also get:

  • IteratorLayout — auto-paginates any collection into a slot region.
  • SliderComponent — a draggable value slider rendered as a track + thumb.
  • ReactiveSlot — a slot whose item is resolved per-player at render time, with an optional onClick callback (live counters, custom buttons).

State-driven widgets built on ReactiveSlot (auto-repaint on click):

  • toggle(x, y, state, onItem, offItem, onToggle) — a boolean switch; the caller owns the persisted state, the widget reads and flips it.
  • progressBar(x, y, width, progress, filled, empty, partial?) — a 0.0–1.0 bar spanning width slots with an optional boundary-cell item.
  • tabs(x, y, tabs, active, onSelect) — a horizontal tab strip; each TabSpec defines its active/inactive item.
menu.toggle(
x = 4, y = 1,
state = stateOf { player -> settings.isPvpEnabled(player) },
onItem = pvpOnItem, offItem = pvpOffItem,
onToggle = { player, enabled -> settings.setPvp(player, enabled) },
)