Custom Pages
Custom pages are the primary system for creating menus, dialogs, forms, and other interactive interfaces.
Page Classes
Section titled “Page Classes”| Class | Purpose |
|---|---|
CustomUIPage | Abstract base class |
InteractiveCustomUIPage<T> | Adds typed event handling (most common) |
BasicCustomUIPage | Simplified version without typed events |
Creating a Page
Section titled “Creating a Page”Extend InteractiveCustomUIPage<T> where T is your event data class:
public class MyPage extends InteractiveCustomUIPage<MyPage.EventData> {
public MyPage(PlayerRef playerRef) { super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); }
@Override public void build(Ref<EntityStore> ref, UICommandBuilder cmd, UIEventBuilder events, Store<EntityStore> store) { // load .ui file cmd.append("Pages/MyPage.ui");
// set initial values cmd.set("#Title.Text", "Welcome!");
// bind events events.addEventBinding( CustomUIEventBindingType.Activating, "#CloseButton", EventData.of("Action", "close") ); }
@Override public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, EventData data) { if ("close".equals(data.action)) { this.close(); } }
// event data class with codec public static class EventData { public static final BuilderCodec<EventData> CODEC = BuilderCodec.builder( EventData.class, EventData::new) .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) .add() .build();
private String action; }}Page Lifecycle
Section titled “Page Lifecycle”- Constructor - Store references, initialize state
build()- Called once when page opens; load UI, set values, bind eventshandleDataEvent()- Called for each client eventsendUpdate()- Push changes to clientonDismiss()- Called when page closes (cleanup)
Page Lifetimes
Section titled “Page Lifetimes”Control how the page can be closed:
| Lifetime | Behavior |
|---|---|
CantClose | Player cannot dismiss; must be closed by server |
CanDismiss | ESC key dismisses the page |
CanDismissOrCloseThroughInteraction | ESC or interaction closes |
super(playerRef, CustomPageLifetime.CantClose, EventData.CODEC);Opening Pages
Section titled “Opening Pages”See Player API for how to obtain a PlayerRef.
Player player = store.getComponent(ref, Player.getComponentType());PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());MyPage page = new MyPage(playerRef);player.getPageManager().openCustomPage(ref, store, page);Closing Pages
Section titled “Closing Pages”// from outside the pageplayer.getPageManager().setPage(ref, store, Page.None);
// from inside the pagethis.close();Updating the UI
Section titled “Updating the UI”State changes require explicit updates:
private int score = 0;
public void addScore(int points) { this.score += points;
// must explicitly push the change UICommandBuilder cmd = new UICommandBuilder(); cmd.set("#Score.Text", String.valueOf(this.score)); this.sendUpdate(cmd, new UIEventBuilder(), false);}The sendUpdate() parameters:
cmd- UI commands to executeevents- New event bindings (or empty builder to keep existing)clear- Iftrue, clears all existing content first
Rebuilding the Page
Section titled “Rebuilding the Page”For major state changes, use rebuild() to re-run build():
// triggers build() again with fresh statethis.rebuild();When to use which:
sendUpdate()- Small changes (update a label, toggle visibility). Faster, only sends diff.rebuild()- Major changes (different layout, add/remove many elements). Re-runsbuild()entirely.
Event Data Codec
Section titled “Event Data Codec”The event data class needs a codec to deserialize JSON from the client. See Codecs for more on the codec system.
public static class EventData { public static final BuilderCodec<EventData> CODEC = BuilderCodec.builder( EventData.class, EventData::new) // static field .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) // dynamic field (@ prefix in binding) .append(new KeyedCodec<>("@SearchQuery", Codec.STRING), (e, s) -> e.searchQuery = s, e -> e.searchQuery) .add() .build();
private String action; private String searchQuery;}Opening Pages from Interactions
Section titled “Opening Pages from Interactions”Register a page to open when players interact with blocks. See Adding Interactions for more on the interaction system.
// create an interaction that opens a pageOpenCustomUIInteraction interaction = new OpenCustomUIInteraction( (playerRef, ref, store) -> new MyPage(playerRef));
// register with your custom block/entity interaction typeExample: Counter Page
Section titled “Example: Counter Page”Complete example with increment/decrement buttons:
public class CounterPage extends InteractiveCustomUIPage<CounterPage.EventData> {
private int count = 0;
public CounterPage(PlayerRef playerRef) { super(playerRef, CustomPageLifetime.CanDismiss, EventData.CODEC); }
@Override public void build(Ref<EntityStore> ref, UICommandBuilder cmd, UIEventBuilder events, Store<EntityStore> store) { cmd.append("Pages/Counter.ui"); cmd.set("#Count.Text", String.valueOf(this.count));
events.addEventBinding( CustomUIEventBindingType.Activating, "#IncrementBtn", EventData.of("Action", "inc") );
events.addEventBinding( CustomUIEventBindingType.Activating, "#DecrementBtn", EventData.of("Action", "dec") ); }
@Override public void handleDataEvent(Ref<EntityStore> ref, Store<EntityStore> store, EventData data) { if ("inc".equals(data.action)) { this.count++; } else if ("dec".equals(data.action)) { this.count--; }
UICommandBuilder cmd = new UICommandBuilder(); cmd.set("#Count.Text", String.valueOf(this.count)); this.sendUpdate(cmd, new UIEventBuilder(), false); }
public static class EventData { public static final BuilderCodec<EventData> CODEC = BuilderCodec.builder( EventData.class, EventData::new) .append(new KeyedCodec<>("Action", Codec.STRING), (e, s) -> e.action = s, e -> e.action) .add() .build();
private String action;
public static EventData of(String key, String value) { EventData data = new EventData(); data.action = value; return data; } }}