Menus
Inventory menus with buttons, layouts, pagination, back navigation, per-player state and confirmation dialogs.
Menus come from bloom-gui. You describe a Menu once and open it for any number of players; each player gets their
own MenuView with its own page and state. Menus.of(bloom) registers the listeners the first time you use it.
A first menu
Items items = Items.of(bloom);
Menu menu = Menu.builder()
.title("&dWarps")
.rows(3)
.button(
11,
Button.of(items.builder("GRASS_BLOCK").name("&aSpawn").build(), click -> {
click.player().performCommand("spawn");
click.close();
}))
.button(
15,
Button.of(
items.builder("DIAMOND_SWORD").name("&cArena").build(),
click -> click.player().performCommand("warp arena")))
.fill(Button.of(
items.builder("GRAY_STAINED_GLASS_PANE").name(" ").build()))
.build();
Menus.of(bloom).open(player, menu);Slots are numbered from 0, left to right, top to bottom. button(row, column, button) is also available.
fill(...) puts a button into every slot that ends up empty.
Layouts
Describe the menu as rows of nine characters and bind characters to buttons. Spaces are empty slots; an unbound character is an error that names the row and column.
Items items = Items.of(bloom);
return Menu.builder()
.title("&d&lBlossom Shop")
.layout("#########", "# h f #", "####x####")
.bind(
'#',
Button.of(items.builder("PINK_STAINED_GLASS_PANE")
.name(" ")
.build()))
.bind('h', heal)
.bind('f', feed)
.bind('x', exit)
.build();Buttons
Button vip = Button.of(vipIcon, click -> click.player().sendMessage("Welcome, VIP"))
.permission("shop.vip", lockedIcon); // players without the permission see lockedIcon
Button nightOnly = Button.of(
items.builder("CLOCK").name("&9Night market").build())
.visibleWhen(view -> view.player().getWorld().getTime() > 13000);
Button health = Button.dynamic(view -> items.builder("GOLDEN_APPLE")
.name("&aHealth: " + (int) view.player().getHealth())
.build());Button.of(item)is a decoration;Button.of(item, handler)is clickable.Button.dynamic(view -> item)computes the item on every render for that viewer.permission(node)hides the button from players without the permission;permission(node, item)showsiteminstead and ignores clicks.visibleWhen(condition)hides the button when the condition is false.
Buttons are immutable, so one button can be shared by many menus.
Clicks
The handler receives a ClickContext: player(), kind() (LEFT, SHIFT_RIGHT, NUMBER_KEY and so on),
slot(), view() and navigation methods. Navigation (open, back, close) happens on the next tick, because
opening or closing inventories inside a click event is unsafe on Bukkit servers. refresh() updates the items
immediately.
Per-player state and refreshing
Button quantity = Button.dynamic(view -> items.builder("GOLD_NUGGET")
.name("&eQuantity: " + view.state().getOrDefault(QUANTITY, 1))
.lore("&7Left click +1, right click -1")
.build())
.onClick(click -> {
int current = click.view().state().getOrDefault(QUANTITY, 1);
int next = click.kind().isRight() ? Math.max(1, current - 1) : current + 1;
click.view().state().set(QUANTITY, next);
click.refresh(); // re-renders this player's view only
});State lives as long as the player's view: it survives refreshes, page changes and returning with back, and is
discarded when the player closes the menu. refreshEvery(ticks) re-renders the menu for every viewer while it is
open, which suits timers.
Pagination
Menu warpList = Menu.builder()
.title("&dWarps &8(page {page} of {pages})")
.apply(MenuTemplates.paged(
6,
Button.of(items.builder("BLACK_STAINED_GLASS_PANE")
.name(" ")
.build()),
items.builder("ARROW").name("&7Previous").build(),
items.builder("ARROW").name("&7Next").build(),
Button.of(items.builder("BARRIER").name("&cBack").build(), click -> click.back())))
.paginate(view -> {
List<Button> buttons = new ArrayList<Button>();
for (final String warp : warps) {
buttons.add(Button.of(
items.builder("ENDER_PEARL").name("&f" + warp).build(),
click -> click.player().performCommand("warp " + warp)));
}
return buttons;
})
.build();The page source runs on every render and returns all buttons; Bloom slices them into pages. Content goes into every
slot without a button, or the slots you pass to contentSlots(...). Previous and next buttons appear only when there
is a page to go to. Titles can use {page}, {pages} and {player}.
Navigation and history
Menu main = Menu.builder()
.title("&dMenu")
.rows(1)
.button(
4,
Button.of(
items.builder("COMPARATOR").name("&7Settings").build(),
click -> click.open(settings))) // opens next tick; this menu goes into the history
.build();
// In the settings menu: Button.of(backIcon, click -> click.back())Opening a menu from another Bloom menu remembers the previous one, up to 16 levels deep. Closing the inventory clears the history.
Confirmation dialogs
Menu confirm = ConfirmMenu.builder()
.title("&cDelete home " + home + "?")
.confirm(items.builder("LIME_WOOL").name("&aDelete").build(), click -> {
click.player().performCommand("delhome " + home);
click.close();
})
.cancel(items.builder("RED_WOOL").name("&cKeep it").build()) // default: back, or close
.build();
Menus.of(bloom).open(player, confirm);Templates
MenuTemplate branded = builder -> builder.border(Button.of(
items.builder("PINK_STAINED_GLASS_PANE").name(" ").build()))
.item(4, logo);
Menu profile = Menu.builder().rows(5).apply(branded).title("&dProfile").build();MenuTemplates.border, MenuTemplates.fill and MenuTemplates.paged cover the common layouts.
Safety
Bloom cancels every click and drag while one of its menus is open: clicks in the menu, clicks in the player's own inventory, shift-clicks, number-key swaps, double-click collection and drags. If another plugin un-cancels a click, Bloom cancels it again at the highest priority and logs one warning. Items can never move into or out of a Bloom menu.
Button handlers run at most once per click. A double click's second event is ignored, and a short per-player debounce stops spam clicking from buying the same thing twice:
Menus menus = Menus.of(bloom).clickDebounce(Duration.ofMillis(150)); // default 100 msOn 1.7 and 1.8 the player's inventory is resent after cancelled clicks, which removes client-side ghost items.
Open menus are closed when the player quits and when your plugin shuts down, and their onClose handlers run.