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.

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) },
)