Skip to content

These docs describe Bloom 0.1.0, a pre-release. APIs may change before 1.0, and Bloom is not on Maven Central yet (see Installation).

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

JavaView source
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.

JavaView source
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

JavaView source
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) shows item instead 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

JavaView source
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

JavaView source
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}.

JavaView source
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

JavaView source
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

JavaView source
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:

JavaView source
Menus menus = Menus.of(bloom).clickDebounce(Duration.ofMillis(150)); // default 100 ms

On 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.