Chapter 1
Start Here
Salt turns full-screen Minecraft inventory screens into desktop windows. Your mod does not create a new screen. It registers a window definition for the menu that Salt already captures or that your server handler opts into.
Mental Model
Salt owns the desktop shell: window frame, focus, movement, pinning, ghost previews, carried stack sync, hotbar and offhand handling, and normal slot click routing.
Your integration owns the inside of one window: its title, content size, widgets, real slot positions, fake item entries, custom input, tooltips, and any session-scoped payloads.
Decision Path
- Does the feature use a real server menu? If not, keep it outside Salt or build a custom session model first.
- Are the displayed items real slots? Use Salt slot routing for real slots and virtual entries for fake items.
- Does the screen send custom packets? Convert screen-global packets to Salt session payloads.
- Does the texture include player inventory? Crop the machine area and do not draw duplicated player inventory.
What You Usually Build
public final class MyCompat {
public static void initClient() {
SaltsInventoryDesktopApi.registerClientWindow(MyMenus.MY_MENU, new MyWindow());
}
public static void initServer() {
SaltsInventoryDesktopApi.registerServerWindow(MyMenus.MY_MENU, new MyServerWindow());
}
}
A simple storage menu might only need real slot positions. A furnace-like machine usually also needs a cropped background, progress textures, and a recipe-book button. A terminal often needs virtual entries and custom payloads.
Chapter 2
Compatibility Triage
Triage decides whether a menu can become a Salt desktop window now, needs rework, or should stay on its original screen until the API grows.
Showstoppers
If any box applies, write the compat layer around that behavior first. Unknown modded menus should keep opening their original screens until the integration is explicit.
Pick A Pattern
| Menu shape | Use | Avoid |
|---|---|---|
| Plain slot grid | renderSlot, slotAt, fixed or storage-grid resize |
Drawing a duplicate player inventory |
| Machine or furnace-like | Cropped background, texturelessSlot, progress textures |
Copying the whole vanilla screen texture |
| Terminal with fake entries | DesktopVirtualItem, custom payloads, search widgets |
Returning DesktopSlotHit for non-slots |
| Screen with custom controls | Widget helpers, menu buttons, typed payloads | Global packets that do not name the Salt session |
Rework Checklist
- Extract the machine/container UI from the old
Screenrenderer. - Move client-only UI state into the desktop state object or synced data.
- Replace screen-global packets with Salt payloads for session-specific actions.
- Return real menu slot ids from
slotAt. - Test normal click, shift-click, drag distribution, double-click, carried stacks, ghost pin, and reopen.
Chapter 3
Registration
Client registration describes how a menu looks. Server registration decides whether Salt may capture that menu as a live desktop session.
Basic Registration
public static void initClient() {
MenuType<MyMenu> menu = findMenu("other_mod:my_menu");
if (menu != null) {
SaltsInventoryDesktopApi.registerClientWindow(menu, new MyWindow());
}
}
public static void initServer() {
MenuType<MyMenu> menu = findMenu("other_mod:my_menu");
if (menu != null) {
SaltsInventoryDesktopApi.registerServerWindow(menu, new MyServerWindow());
}
}
Register both sides for a captured modded menu. A client-only definition can exist, but it does not make an unknown server menu desktop-managed by itself.
Predicate Registration
SaltsInventoryDesktopApi.registerClientWindowPredicate(
Identifier.fromNamespaceAndPath("my_mod", "dynamic_storage"),
100,
context -> context.sourceKey().startsWith("block:"),
context -> new DynamicStorageWindow()
);
Use predicates when one MenuType has multiple layouts or when the source key decides which
desktop window should be used.
Optional Compat
private static @Nullable MenuType<?> findMenu(String id) {
Identifier key = Identifier.tryParse(id);
return key == null ? null : BuiltInRegistries.MENU.getOptional(key).orElse(null);
}
Keep optional mod references guarded. Prefer registry ids, optional entrypoints, or small reflection helpers over hard class references in always-loaded code.
Chapter 4
Client Window
The client definition is the render and input adapter. It should describe the content area, not re-create Minecraft's full screen.
Lifecycle Hooks
| Hook | Use for |
|---|---|
createState | Search text, selected tab, scroll row, animation state. |
title | Window title based on the menu or local state. |
defaultSize | Full Salt window size, including title bar and padding. |
render | Slots, textures, text, widgets, virtual items. |
slotAt | Real menu slot hitboxes only. |
saveLocalState | Local UI state that should survive reopen. |
Minimal Slot Window
public final class MyWindow implements DesktopWindowDefinition<MyMenu, MyWindow.State> {
public static final class State {
int scroll;
}
@Override
public State createState(DesktopWindowSetupContext<MyMenu> context) {
return new State();
}
@Override
public DesktopWindowSize defaultSize(DesktopWindowSetupContext<MyMenu> context) {
return DesktopWindowSize.of(8 + 9 * 18 + 8, 16 + 8 + 3 * 18 + 8);
}
@Override
public void render(DesktopRenderContext<MyMenu, State> context) {
int x = context.contentX();
int y = context.contentY();
for (int i = 0; i < 27; i++) {
context.renderSlot(i, x + i % 9 * 18, y + i / 9 * 18);
}
}
}
Window Size vs Content Size
Start drawing at context.contentX() and context.contentY(). Return a
defaultSize large enough for title bar, padding, and content. In current examples that is
usually 8 + contentWidth + 8 wide and 16 + 8 + contentHeight + 8 tall.
Chapter 5
Slots And Input
Normal item movement should flow through Salt's slot routing. Custom UI controls should consume input only when they actually handled it.
Real Slot Routing
@Override
public DesktopSlotHit slotAt(DesktopSlotContext<MyMenu, State> context, double mouseX, double mouseY) {
int x = context.contentX();
int y = context.contentY();
DesktopSlotHit input = context.hitSlot(0, x + 56, y + 17, mouseX, mouseY);
if (input != null) return input;
return context.hitSlot(1, x + 116, y + 35, mouseX, mouseY);
}
Return the index into menu.slots, not visual order. If you think in container slot indexes, use
containerSlot(...), containerSlotHit(...), and menuSlotId(...).
Slot Rendering Choices
| Helper | Use when |
|---|---|
renderSlot(id, x, y) | Salt should draw slot background, item, hover, and drag preview. |
slot(slot, x, y) | You already resolved the Slot object. |
texturelessSlot(id, x, y) | Your background already contains slot art. |
slotBackground(x, y) | You want Salt's slot art but need custom item rendering order. |
Text Input
public static final class State {
final DesktopTextBoxState search = new DesktopTextBoxState();
}
@Override
public void render(DesktopRenderContext<MyMenu, State> context) {
DesktopWidgets.renderTextBox(context, context.state().search, context.contentX(), context.contentY(), 120);
}
@Override
public boolean mouseClicked(DesktopInputContext<MyMenu, State> context, MouseButtonEvent event, boolean doubleClick) {
return DesktopWidgets.clickTextBox(context.state().search, event, context.contentX(), context.contentY(), 120);
}
@Override
public boolean wantsTextInput(DesktopWindowContext<MyMenu, State> context) {
return DesktopWidgets.wantsTextInput(context.state().search);
}
Always implement wantsTextInput for focused text boxes. That tells Salt to suppress movement
and normal UI hotkeys while the player is typing.
Virtual Items
List<DesktopVirtualItem> visible = entries.stream()
.map(entry -> new DesktopVirtualItem(entry.stack(), entry.count()))
.toList();
DesktopWidgets.renderVirtualItemGrid(context, visible, firstIndex, gridX, gridY, columns, rows);
int index = DesktopWidgets.virtualItemIndexAt(
event.x(), event.y(), gridX, gridY, columns, rows, firstIndex, visible.size()
);
if (index >= 0) {
context.sendPayload(PULL_CHANNEL, new PullPayload(index), PullPayload.CODEC);
return true;
}
Virtual entries are not slots. Do not return DesktopSlotHit for them.
Chapter 6
Server And Payloads
Server handlers opt menus into capture and keep custom session behavior synchronized. Payloads are for custom actions that vanilla slot clicks and menu buttons cannot express.
Server Handler
public final class MyServerWindow implements DesktopServerWindowHandler<MyMenu, MyServerWindow.State> {
public static final class State {
long lastHash;
}
@Override
public State createState(DesktopServerSessionContext<MyMenu, State> context) {
return new State();
}
@Override
public void tick(DesktopServerSessionContext<MyMenu, State> context) {
context.broadcastChanges();
}
}
Hidden ghost sessions still tick and validate through the desktop session manager. closed means
the live session is really ending, not just becoming a ghost preview.
Typed And Raw Payloads
public record ModePayload(int mode) {
public static final StreamCodec<RegistryFriendlyByteBuf, ModePayload> CODEC =
StreamCodec.composite(ByteBufCodecs.INT, ModePayload::mode, ModePayload::new);
}
SaltsInventoryDesktopApi.registerServerPayload(MyMenus.MY_MENU, MY_CHANNEL, ModePayload.CODEC, (context, payload) -> {
context.menu().setMode(payload.mode());
context.broadcastChanges();
});
context.sendPayload(MY_CHANNEL, new ModePayload(2), ModePayload.CODEC);
SaltsInventoryDesktopApi.registerServerPayload(MyMenus.MY_MENU, MY_CHANNEL, context -> {
byte[] data = context.data();
int mode = decodeMode(data);
context.menu().setMode(mode);
context.broadcastChanges();
});
context.sendPayload(MY_CHANNEL, encodeMode(2));
Keep payloads small and session-scoped. Salt's networking layer is designed around a 32 KiB cap.
When To Use Payloads
- Use normal slot clicks for real inventory slots.
- Use
sendMenuButtonfor vanilla-style menu buttons. - Use payloads for selected tabs, terminal actions, custom sort modes, fake entries, and special server commands.
Chapter 7
UI Patterns
These examples cover common layouts: cropped machine textures, recipe-book buttons, resizing, and desktop window behavior that Salt inherits automatically.
Cropped Furnace-Like Background
private static final Identifier MY_SCREEN_TEXTURE =
Identifier.fromNamespaceAndPath("my_mod", "textures/gui/my_machine.png");
private static final int TEXTURE_WIDTH = 256;
private static final int TEXTURE_HEIGHT = 256;
private static final int MACHINE_WIDTH = 176;
private static final int MACHINE_HEIGHT = 84;
@Override
public DesktopWindowSize defaultSize(DesktopWindowSetupContext<MyMenu> context) {
return DesktopWindowSize.of(8 + MACHINE_WIDTH + 8, 16 + 8 + MACHINE_HEIGHT + 8);
}
@Override
public void render(DesktopRenderContext<MyMenu, State> context) {
int x = context.contentX();
int y = context.contentY();
context.texture(MY_SCREEN_TEXTURE, x, y, 0, 0,
MACHINE_WIDTH, MACHINE_HEIGHT,
MACHINE_WIDTH, MACHINE_HEIGHT,
TEXTURE_WIDTH, TEXTURE_HEIGHT);
context.texturelessSlot(0, x + 56, y + 17);
context.texturelessSlot(1, x + 56, y + 53);
context.texturelessSlot(2, x + 116, y + 35);
}
Use texturelessSlot only when your cropped texture already has slot backgrounds. Otherwise use
renderSlot or draw slotBackground first.
Recipe Book
@Override
public RecipeBookComponent<?> createRecipeBook(DesktopWindowContext<MyMenu, State> context) {
return MyClientReflect.createRecipeBook(context.menu());
}
@Override
public boolean mouseClicked(DesktopInputContext<MyMenu, State> context, MouseButtonEvent event, boolean doubleClick) {
if (recipeButton.contains(event.x(), event.y())) {
context.toggleRecipeBook();
return true;
}
return false;
}
On 1.20.1 and 1.21.1, return raw RecipeBookComponent instead of
RecipeBookComponent<?>.
Resize And Snap
@Override
public DesktopResizePolicy resizePolicy(DesktopWindowContext<MyMenu, State> context) {
return DesktopResizePolicy.STORAGE_GRID;
}
@Override
public DesktopWindowSize snapSize(DesktopWindowContext<MyMenu, State> context) {
int columns = Math.max(1, context.contentWidth() / 18);
int rows = Math.max(1, context.contentHeight() / 18);
return DesktopWindowSize.of(8 + columns * 18 + 8, 16 + 8 + rows * 18 + 8);
}
Use fixed windows for functional machine UIs. Use storage-grid resizing only when the window can add or remove visible rows or columns cleanly.
Inherited Desktop Behavior
Do not rebuild these features in your definition. Add hooks such as ghosted() only for local
visual state or preview-specific effects.
Chapter 8
Troubleshooting
Most integration failures come from missing server support, wrong slot ids, global packets, or rendering the old screen unchanged.
Common Failure Modes
Window does not open in Salt
Confirm the menu type was found, the client definition registered, and the server handler or payload handler registered for unknown modded menus. Unknown menus fall back to their original screen.
Clicking a slot moves the wrong item
Check that slotAt returns the menu slot id. Container slot indexes and visual slot order are
not always the same thing.
Payload never reaches the server
Verify the payload channel id, codec/raw encoding, server registration, and that the action belongs to the same menu type as the captured session.
Player inventory appears twice
Crop the machine portion of the texture and remove player inventory slots from the custom render path. Salt already handles player inventory and hotbar interaction.
Typing also moves the player
Implement wantsTextInput and return true while a text box is focused.
Debug Checklist
- Log the menu registry id and slot count when registering.
- Render temporary labels for menu slot ids while aligning hitboxes.
- Test one slot click before adding custom widgets.
- Test one payload before adding complex state syncing.
- Compare behavior with the original screen for shift-click and result slots.
Chapter 9
Reference
Keep this chapter nearby once the integration works. It summarizes the API surface and version differences.
Context Quick Reference
DesktopWindowContext
menu, sessionId, state, contentX/Y, containerSlot, menuSlotId
DesktopRenderContext
fill, text, texture, sprite, slot, texturelessSlot, virtualItem
DesktopInputContext
sendMenuButton, clickSlot, quickMoveSlot, sendPayload, toggleRecipeBook
DesktopWidgets
Text boxes, icon buttons, text buttons, scrollbars, dropdowns, virtual item grids.
Version Summary
| Band | Important differences |
|---|---|
| 26.1.2 and 26.2 | Identifier, ContainerInput, typed payload helpers. |
| 1.21.11 | Identifier, ClickType, typed payload helpers. |
| 1.21.1 | ResourceLocation, Salt compatibility input package, raw RecipeBookComponent. |
| 1.20.1 | ResourceLocation, raw byte payloads only. |
No matching chapters or sections.