Folio — Documentation
1. Layouts
Mask layout — text blocks
Code (Java):
.
layout
(MaskLayout.
of
(
""
"
BBBBBBBBB
B XXXXX B
B XXXXX B
B XXXXX B
BBBBBBBBB
"
""
)
)
| X |
item slot (consumed by auto-pagination) |
| B |
border (theme material, no click) |
| . |
empty slot |
| < > | F L P |
reserved markers — accessible via MaskLayout.slotOf(...) |
Whitespace is ignored — write the pattern naturally.
Grid layout
Code (Java):
.
layout
(
GridLayout.
bordered
(
)
)
// border + bottom row reserved
.
layout
(
new
GridLayout
(
true,
0
)
)
// border only
.
layout
(
GridLayout.
plain
(
)
)
// 9×rows, no border
Linear layout
Code (Java):
.
layout
(LinearLayout.
full
(
)
)
// 0..size
.
layout
(LinearLayout.
withNavRow
(
)
)
// 0..size-9 (last row reserved)
[HR][/HR]
2. Click handlers
Code (Java):
InventoryItem.
builder
(
)
.
material
(Material.
DIAMOND_SWORD
)
.
name
(
Component.
text
(
"Excalibur"
)
)
.
onLeftClick
(ctx
->
{ upgrade
(ctx.
player
(
)
)
;
return ClickResult.
refresh
(
)
;
}
)
.
onRightClick
(ctx
->
{ describe
(ctx.
player
(
)
)
;
return ClickResult.
cancel
(
)
;
}
)
.
onShiftClick
(ctx
->
{ sell
(ctx.
player
(
)
)
;
return ClickResult.
refresh
(
)
;
}
)
.
onClickAsync
(ctx
->
{ db.
recordClick
(...
)
;
return ClickResult.
cancel
(
)
;
}
)
.
build
(
)
;
ClickResult is a sealed interface — exhaustive switch:
Code (Java):
switch
(handler.
handle
(ctx
)
)
{
case ClickResult.
Allow a
->
{
/* let the underlying click through */
}
case ClickResult.
Cancel c
->
{
/* default — already cancelled */
}
case ClickResult.
Close cl
-> ctx.
player
(
).
closeInventory
(
)
;
case ClickResult.
Refresh r
-> session.
refresh
(
)
;
case ClickResult.
Navigate n
-> session.
switchPage
(n.
targetPage
(
)
)
;
}
Click events bubble:
item handler → item global → page global, stopping at the first non-Cancel result.
[HR][/HR]
3. Animations
Code (Java):
AnimatedItem loading
= AnimatedItem.
builder
(
).
frames
(
List.
of
(
Frame.
of
(named
(Material.
RED_WOOL,
"Loading"
),
4
),
Frame.
of
(named
(Material.
YELLOW_WOOL,
"Loading"
),
4
),
Frame.
of
(named
(Material.
LIME_WOOL,
"Loading"
),
4
)
)
).
build
(
)
;
InventoryPage.
builder
(
)
.
refreshEvery
(Duration.
ofMillis
(
50
)
)
// 1 tick — clamped to >= 1
.
fixed
(
13, loading
)
...
Diff-renderer only writes slots whose stack actually changed —
no flicker.
[HR][/HR]
4. Async loading
Code (Java):
AsyncSpec spec
= AsyncSpec.
of
(
(
)
-> CompletableFuture.
supplyAsync
(
(
)
->
database.
loadShopOffers
(playerId
)
.
stream
(
)
.
map
(
this
::toItem
)
.
toList
(
)
)
)
.
placeholder
(InventoryItem.
of
(named
(Material.
CLOCK,
"§7Loading…"
)
)
)
.
errorItem
(InventoryItem.
of
(named
(Material.
BARRIER,
"§cError"
)
)
)
.
cache
(Duration.
ofMinutes
(
5
)
)
.
cacheKey
(
"shop:"
+ playerId
)
.
build
(
)
;
InventoryPage.
builder
(
).
asyncItems
(spec
)...
Inventory opens
instantly with placeholders. When the future resolves, items pop in on the main thread (diffed). If the player closes mid-load, the pending future is soft-discarded.
[HR][/HR]
5. Filter & live search
Code (Java):
// Predicate-based filter
Filter cheap
=
(item, viewer
)
-> price
(item
)
< viewer.
getLevel
(
)
;
session.
setFilter
(cheap.
and
(byCategory
(
"weapons"
)
)
)
;
// Anvil-GUI live search
SearchPrompt.
builder
(plugin, player
)
.
title
(
Component.
text
(
"Search", NamedTextColor.
GOLD
)
)
.
initial
(
""
)
.
onConfirm
(query
->
{
String q
= query.
toLowerCase
(
)
;
Filter f
=
(i, v
)
-> i.
render
(v
).
getType
(
).
name
(
).
toLowerCase
(
).
contains
(q
)
;
var s
= registry.
open
(player, paged
)
;
(
(InventorySessionImpl
) s
).
setFilter
(f
)
;
}
)
.
onCancel
(
(
)
-> registry.
open
(player, paged
)
)
.
open
(
)
;
[HR][/HR]
6. Persistence
Code (Java):
// 1) Register a named factory
registry.
registerNamed
(
"shop",
(
)
-> PagedInventory.
paginate
(buildShopPage
(
)
)
)
;
// 2) Mark the page persistent
InventoryPage.
builder
(
)
.
persistent
(
"shop"
)
// saved to PDC on close
...
After a re-login, Folio reads the player's PDC and reopens the same page on the same index — automatic.
[HR][/HR]
7. Hotbar keybinds
Code (Java):
InventoryPage.
builder
(
)
.
hotbarBind
(
1, ctx
->
{ ctx.
player
(
).
sendMessage
(
"Quick action 1"
)
;
return ClickResult.
cancel
(
)
;
}
)
.
hotbarBind
(
2, ctx
-> ClickResult.
refresh
(
)
)
.
hotbarBind
(
3, ctx
-> ClickResult.
close
(
)
)
...
While the player hovers any slot in your top inventory, pressing 1..9 fires the matching handler.
[HR][/HR]
8. Form / input slots
Code (Java):
InventoryPage.
builder
(
)
.
inputSlots
(
11,
13,
15
)
// player can place items here
.
fixed
(
22, InventoryItem.
builder
(
)
.
material
(Material.
EMERALD
).
name
(
Component.
text
(
"Confirm"
)
)
.
onClick
(ctx
->
{
var inv
= ctx.
session
(
).
inventoryFor
(ctx.
player
(
)
)
;
var ingredients
=
List.
of
(inv.
getItem
(
11
), inv.
getItem
(
13
), inv.
getItem
(
15
)
)
;
if
(ingredients.
stream
(
).
anyMatch
(i
-> i
==
null
|| i.
getType
(
).
isAir
(
)
)
)
{
ctx.
player
(
).
sendMessage
(
"Fill all 3 slots!"
)
;
return ClickResult.
cancel
(
)
;
}
craft
(ctx.
player
(
), ingredients
)
;
inv.
setItem
(
11,
null
)
; inv.
setItem
(
13,
null
)
; inv.
setItem
(
15,
null
)
;
return ClickResult.
refresh
(
)
;
}
)
.
build
(
)
)
.
build
(
)
;
On close, leftover items in input slots are returned to the player's inventory (or dropped if full) — players can't lose stuff.
[HR][/HR]
9. Multi-player (shared) sessions
Code (Java):
InventoryPage.
builder
(
)
.
sessionMode
(SessionMode.
SHARED
)
// one Inventory, many viewers
.
title
(
Component.
text
(
"Auction"
)
)
...
All viewers see the same Inventory — clicks by one are visible to all. Page cursor is also shared.
[HR][/HR]
10. Theme
Code (Java):
InventoryTheme theme
= InventoryTheme.
builder
(
)
.
borderMaterial
(Material.
BLACK_STAINED_GLASS_PANE
)
.
openSound
(Sound.
UI_TOAST_IN
)
.
clickSound
(Sound.
UI_BUTTON_CLICK
)
.
pageSwitchSound
(Sound.
ITEM_BOOK_PAGE_TURN
)
.
deniedSound
(Sound.
BLOCK_NOTE_BLOCK_BASS
)
.
pageIndicatorTemplate
(
"<dark_gray>« <white><current><dark_gray>/<white><total> <dark_gray>»"
)
.
build
(
)
;
InventoryPage.
builder
(
).
theme
(theme
)...
Indicator template is
MiniMessage with <current> and <total> tokens.
[HR][/HR]
11. Permissions
Code (Java):
InventoryItem.
builder
(
)
.
visibleIf
(PermissionGate.
hasPermission
(
"shop.see-rare"
)
)
.
clickableIf
(PermissionGate.
all
(
PermissionGate.
hasPermission
(
"shop.buy"
),
p
-> economy.
balance
(p
)
>
0
)
)
...
InventoryPage.
builder
(
)
.
requirePermission
(
"shop.use"
)
// page-level gate
...
Hidden items are excluded from layout placement and don't count toward pagination.
[HR][/HR]
12. Complete example — all features in one page
Code (Java):
InventoryPage page
= InventoryPage.
builder
(
)
.
title
(
Component.
text
(
"Big Demo", NamedTextColor.
GOLD
)
)
.
rows
(
6
)
.
layout
(MaskLayout.
of
(
""
"
BBBBBBBBB
BXXXXXXXB
BXXXXXXXB
BXXXXXXXB
BXXXXXXXB
BBBBBBBBB
"
""
)
)
.
theme
(InventoryTheme.
dark
(
)
)
.
navigation
(NavigationButtons.
builder
(
)
.
firstSlot
(
45
).
previousSlot
(
46
).
pageIndicatorSlot
(
48
)
.
nextSlot
(
50
).
lastSlot
(
51
).
build
(
)
)
.
items
(offers
)
.
filter
(
(i, v
)
-> v.
hasPermission
(
"shop.see-all"
)
||
!rare
(i
)
)
.
asyncItems
(databaseSpec
)
.
refreshEvery
(Duration.
ofSeconds
(
1
)
)
.
persistent
(
"big-demo"
)
.
hotbarBind
(
1, ctx
-> ClickResult.
refresh
(
)
)
.
hotbarBind
(
9, ctx
-> ClickResult.
close
(
)
)
.
inputSlots
(
52
)
// single "drop here to sell" slot
.
sessionMode
(SessionMode.
PER_PLAYER
)
.
requirePermission
(
"shop.use"
)
.
onClick
(ctx
-> ClickResult.
cancel
(
)
)
// fallback
.
build
(
)
;
registry.
open
(player, PagedInventory.
paginate
(page
)
)
;