The Menu Handler

The Menu Handler

MenuHandler<C> is the optional hook surface for open, close, click, and drag events. Use it when a menu needs lifecycle logic in addition to rendering.

If you want one type that combines menu layout and lifecycle hooks, implement:

  • PaperInteractiveMenu on Paper
  • InteractiveMenu<String> on Spigot

A Simple Guarded Menu

{`

public final class ConfirmMenu implements PaperInteractiveMenu {

@Override
public Component title(MenuView<Component, ?> view) {
    return Component.text("Confirm");
}

@Override
public Capacity capacity(MenuView<Component, ?> view) {
    return Capacity.ofRows(3);
}

@Override
public Content content(MenuView<Component, ?> view) {
    return Content.builder(view.capacity())
        .set(1, 4, Button.clickable(
            new ItemStack(Material.EMERALD_BLOCK),
            (menuView, event) ->
                menuView.viewer().sendMessage(Component.text("Confirmed"))
        ))
        .build();
}

@Override
public boolean onPreClick(MenuView<Component, ?> view, InventoryClickEvent event) {
    Slot confirmSlot = Slot.at(1, 4, view.capacity());
    if (!Slot.of(event.getSlot()).equals(confirmSlot)) {
        view.viewer().sendMessage(Component.text("Click the center button."));
        return false;
    }
    return true;
}

@Override
public void onOpen(MenuView<Component, ?> view, InventoryOpenEvent event) {
    view.viewer().sendMessage(Component.text("Menu opened"));
}

@Override
public void onClose(MenuView<Component, ?> view, InventoryCloseEvent event) {
    view.viewer().sendMessage(Component.text("Menu closed"));
}

} }</CodeTabItem> <CodeTabItem value="spigot" label="Spigot 1.8.8" language="java">{

public final class ConfirmMenu implements InteractiveMenu {

@Override
public String title(MenuView<String, ?> view) {
    return ChatColor.GOLD + "Confirm";
}

@Override
public Capacity capacity(MenuView<String, ?> view) {
    return Capacity.ofRows(3);
}

@Override
public Content content(MenuView<String, ?> view) {
    return Content.builder(view.capacity())
        .set(1, 4, Button.clickable(
            new ItemStack(Material.EMERALD_BLOCK),
            (menuView, event) ->
                menuView.viewer().sendMessage(ChatColor.GREEN + "Confirmed")
        ))
        .build();
}

@Override
public boolean onPreClick(MenuView<String, ?> view, InventoryClickEvent event) {
    Slot confirmSlot = Slot.at(1, 4, view.capacity());
    if (!Slot.of(event.getSlot()).equals(confirmSlot)) {
        view.viewer().sendMessage(ChatColor.RED + "Click the center button.");
        return false;
    }
    return true;
}

@Override
public void onOpen(MenuView<String, ?> view, InventoryOpenEvent event) {
    view.viewer().sendMessage(ChatColor.YELLOW + "Menu opened");
}

@Override
public void onClose(MenuView<String, ?> view, InventoryCloseEvent event) {
    view.viewer().sendMessage(ChatColor.YELLOW + "Menu closed");
}

} `}

The Real Click Flow

For clicks in the top menu inventory, Lotus behaves like this:

Lotus listener cancels the top-inventory click
-> onPreClick(view, event)
-> if true: button.dispatch(...)
-> Lotus repaints the inventory
-> onPostClick(view, event)

If onPreClick(...) returns false, Lotus stops there.

onPostClick(...) runs only when onPreClick(...) returned true.

What Lotus Already Does For You

  • top-inventory clicks are cancelled before dispatch
  • drag events over the menu are cancelled
  • the clicked button is looked up and dispatched
  • the inventory is repainted after dispatch

That means MenuHandler is for behavior, not for basic protection.

One Important Limitation

Clicks in the player inventory below the menu do not go through MenuHandler. Those are controlled by Lotus.Options.allowBottomInventoryClick(...).

If your design depends on completely freezing the player's own inventory while the menu is open, set allowBottomInventoryClick(false) when you create the Lotus runtime.

When To Use Each Hook

Hook Best use
onPreClick Gate or deny a click before button logic runs
onPostClick Follow-up logic after a successful dispatch
onOpen Initialize view-local state or notify the player
onClose Cleanup or commit data
onDrag React to drag attempts over the menu

Next: Button Variants.