GUI and Overlay Rendering
GUI and Overlay Rendering
Section titled “GUI and Overlay Rendering”GUI and overlay rendering allows you to create custom user interfaces, HUD elements, and interactive screens for your mods. This guide covers creating custom GUI components, overlays, and interactive elements in Minecraft 26.1.
Overview
Section titled “Overview”The GUI system in Minecraft 26.1 uses a component-based architecture:
- GuiGraphics: Central rendering engine for all UI operations
- Screen: Base class for all UI screens and menus
- Gui: Manager for in-game HUD elements
- AbstractWidget: Base class for interactive UI components
- Event System: Manages input handling and component interactions
Core Architecture
Section titled “Core Architecture”GuiGraphics - The Rendering Engine
Section titled “GuiGraphics - The Rendering Engine”GuiGraphics provides the primary interface for all GUI rendering operations:
// Example: Basic drawing operations with GuiGraphicspublic class BasicGuiRendering { public void drawBasicElements(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { // Draw filled rectangle graphics.fill(10, 10, 100, 60, 0xFF333333);
// Draw border graphics.renderOutline(10, 10, 100, 60, 0xFFFFFFFF);
// Draw text graphics.drawString(Minecraft.getInstance().font, Component.literal("Custom GUI"), 15, 20, 0xFFFFFF);
// Draw textured element graphics.blitSprite(ResourceLocation.withDefaultNamespace("widget/button"), 110, 10, 50, 20); }
public void drawTransformedElement(GuiGraphics graphics, int x, int y, float rotation) { // Apply transformations graphics.pose().pushMatrix(); graphics.pose().translate(x + 25, y + 25, 0); graphics.pose().mulPose(Axis.ZP.rotationDegrees(rotation)); graphics.pose().translate(-25, -25, 0);
// Draw transformed element graphics.fill(0, 0, 50, 50, 0xFFFF5555);
graphics.pose().popMatrix(); }}Screen Base Class
Section titled “Screen Base Class”All custom screens extend the Screen class:
// Example: Custom screen implementationpublic class CustomModScreen extends Screen { private CustomProgressBar progressBar; private CycleButton<String> modeButton; private TextFieldWidget textField;
public CustomModScreen() { super(Component.literal("Custom Mod Interface")); }
@Override protected void init() { // Clear existing widgets this.clearWidgets();
// Initialize custom components this.progressBar = new CustomProgressBar( this.width / 2 - 100, this.height / 2 - 50, 200, 20, Component.literal("Processing...") );
this.modeButton = CycleButton.builder(String.class) .withValues("Mode A", "Mode B", "Mode C") .withInitialValue("Mode A") .create( this.width / 2 - 50, this.height / 2, 100, 20, Component.literal("Mode"), (button, value) -> onModeChanged(value) );
this.textField = new TextFieldWidget( this.font, this.width / 2 - 75, this.height / 2 + 40, 150, 20, Component.literal("Input") ); this.textField.setMaxLength(50);
// Add widgets to screen this.addRenderableWidget(progressBar); this.addRenderableWidget(modeButton); this.addRenderableWidget(textField);
// Add close button this.addRenderableWidget( Button.builder(Component.literal("Close"), button -> this.onClose()) .pos(this.width / 2 - 50, this.height / 2 + 80) .size(100, 20) .build() ); }
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { // Render background this.renderBackground(graphics, mouseX, mouseY, partialTick);
// Render title graphics.drawCenteredString(this.font, Component.literal("Custom Mod Interface"), this.width / 2, this.height / 2 - 80, 0xFFFFFF);
// Render all widgets super.render(graphics, mouseX, mouseY, partialTick);
// Render additional information if (this.progressBar.getProgress() > 0.5f) { graphics.drawString(this.font, Component.literal("Processing..."), this.width / 2 - 30, this.height / 2 + 120, 0x55FF55); } }
private void onModeChanged(String newMode) { // Handle mode change System.out.println("Mode changed to: " + newMode); this.progressBar.setProgress(0.0f); // Reset progress }}Custom Widgets
Section titled “Custom Widgets”Creating Custom Widgets
Section titled “Creating Custom Widgets”All interactive components should extend AbstractWidget:
// Example: Custom progress bar widgetpublic class CustomProgressBar extends AbstractWidget { private float progress = 0.0f; private int barColor = 0xFF55FF55; private int backgroundColor = 0xFF333333; private boolean showPercentage = true;
public CustomProgressBar(int x, int y, int width, int height, Component message) { super(x, y, width, height, message); }
@Override protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { // Draw background graphics.fill(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, this.backgroundColor);
// Draw border graphics.renderOutline(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, 0xFFFFFFFF);
// Draw progress fill int progressWidth = (int)(this.width * this.progress); graphics.fill(this.getX(), this.getY(), this.getX() + progressWidth, this.getY() + this.height, this.barColor);
// Draw text/percentage if (this.showPercentage) { Component progressText = Component.literal(String.format("%.1f%%", this.progress * 100.0f)); int textWidth = this.font.width(progressText); int textX = this.getX() + (this.width - textWidth) / 2; int textY = this.getY() + (this.height - this.font.lineHeight) / 2;
graphics.drawString(this.font, progressText, textX, textY, 0xFFFFFF); } }
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { if (this.isMouseOver(event.x(), event.y())) { if (event.button() == 0) { // Left click // Reset progress on click this.setProgress(0.0f); return true; } else if (event.button() == 1) { // Right click // Set to full on right click this.setProgress(1.0f); return true; } } return super.mouseClicked(event, doubleClick); }
@Override protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { if (this.isFocused() && this.isMouseOver(mouseX, mouseY)) { // Update progress based on drag float relativeX = (float)(mouseX - this.getX()) / this.width; this.setProgress(Mth.clamp(relativeX, 0.0f, 1.0f)); } }
// Utility methods public void setProgress(float progress) { this.progress = Mth.clamp(progress, 0.0f, 1.0f); }
public float getProgress() { return this.progress; }
public void setBarColor(int color) { this.barColor = color; }}Advanced Custom Widget
Section titled “Advanced Custom Widget”// Example: Custom draggable panel widgetpublic class DraggablePanel extends AbstractWidget { private boolean isDragging = false; private double dragOffsetX = 0; private double dragOffsetY = 0; private final List<AbstractWidget> children = new ArrayList<>(); private final int headerColor = 0xFF444466; private final int borderColor = 0xFF666666;
public DraggablePanel(int x, int y, int width, int height, Component title) { super(x, y, width, height, title); }
@Override protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { // Draw panel background graphics.fill(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, 0xAA000000);
// Draw header graphics.fill(this.getX(), this.getY(), this.getX() + this.width, this.getY() + 25, this.headerColor);
// Draw borders graphics.renderOutline(this.getX(), this.getY(), this.getX() + this.width, this.getY() + this.height, this.borderColor);
// Draw title graphics.drawString(this.font, this.getMessage(), this.getX() + 5, this.getY() + 5, 0xFFFFFF);
// Draw resize handle graphics.fill(this.getX() + this.width - 10, this.getY() + this.height - 10, this.getX() + this.width, this.getY() + this.height, 0xFF888888);
// Render child widgets graphics.pose().pushMatrix(); graphics.pose().translate(this.getX(), this.getY(), 0);
int childMouseX = mouseX - this.getX(); int childMouseY = mouseY - this.getY();
for (AbstractWidget child : children) { child.render(graphics, childMouseX, childMouseY, partialTick); }
graphics.pose().popMatrix(); }
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { if (this.isMouseOver(event.x(), event.y())) { // Check if clicking on header for dragging if (event.y() - this.getY() < 25) { this.isDragging = true; this.dragOffsetX = event.x() - this.getX(); this.dragOffsetY = event.y() - this.getY(); return true; }
// Check if clicking on resize handle if (event.x() > this.getX() + this.width - 10 && event.y() > this.getY() + this.height - 10) { // Handle resizing (simplified example) return true; }
// Pass click to children for (AbstractWidget child : children) { if (child.mouseClicked(event, doubleClick)) { return true; } } } return super.mouseClicked(event, doubleClick); }
@Override protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { if (this.isDragging) { this.setX((int)(mouseX - this.dragOffsetX)); this.setY((int)(mouseY - this.dragOffsetY)); } }
@Override public boolean mouseReleased(MouseButtonEvent event) { this.isDragging = false; return super.mouseReleased(event); }
public void addChild(AbstractWidget child) { this.children.add(child); }
public void removeChild(AbstractWidget child) { this.children.remove(child); }}HUD and Overlay Integration
Section titled “HUD and Overlay Integration”Custom HUD Overlay
Section titled “Custom HUD Overlay”// Example: Custom HUD overlay implementationpublic class CustomHudOverlay { private final Minecraft minecraft = Minecraft.getInstance(); private boolean showHud = true; private int hudX = 10; private int hudY = 50;
public void render(GuiGraphics graphics, DeltaTracker deltaTracker) { if (!this.showHud || this.minecraft.options.hideGui) { return; }
Player player = this.minecraft.player; if (player == null) { return; }
// Draw custom HUD elements drawPlayerStats(graphics, player); drawCustomCompass(graphics); drawMiniMap(graphics, player); drawCustomEffects(graphics, player); }
private void drawPlayerStats(GuiGraphics graphics, Player player) { int y = this.hudY;
// Player health with custom styling Component healthText = Component.literal("Health: ") .append(Component.literal(String.valueOf((int)player.getHealth())) .withStyle(Style.EMPTY.withColor(ChatFormatting.RED)));
graphics.drawString(this.minecraft.font, healthText, this.hudX, y, 0xFFFFFF); y += 15;
// Player experience Component expText = Component.literal("EXP: ") .append(Component.literal(String.valueOf(player.experienceProgress * 100)) .withStyle(Style.EMPTY.withColor(ChatFormatting.GREEN)));
graphics.drawString(this.minecraft.font, expText, this.hudX, y, 0xFFFFFF); y += 15;
// Current biome ResourceLocation biomeId = this.minecraft.level.getBiome(player.blockPosition()) .value().getRegistryName(); if (biomeId != null) { Component biomeText = Component.literal("Biome: ") .append(Component.literal(biomeId.getPath()) .withStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW)));
graphics.drawString(this.minecraft.font, biomeText, this.hudX, y, 0xFFFFFF); } }
private void drawCustomCompass(GuiGraphics graphics) { int compassX = graphics.guiWidth() - 80; int compassY = 50;
Player player = this.minecraft.player; if (player == null) return;
float yaw = player.getYRot();
// Draw compass background graphics.fill(compassX, compassY, compassX + 60, compassY + 60, 0x66000000); graphics.renderOutline(compassX, compassY, compassX + 60, compassY + 60, 0xFFFFFFFF);
// Draw compass direction graphics.pose().pushMatrix(); graphics.pose().translate(compassX + 30, compassY + 30, 0); graphics.pose().mulPose(Axis.ZP.rotationDegrees(yaw));
// Draw north indicator graphics.fill(-2, -25, 2, -20, 0xFFFF0000);
graphics.pose().popMatrix();
// Draw N/E/S/W labels graphics.drawString(this.minecraft.font, "N", compassX + 27, compassY + 2, 0xFF0000); graphics.drawString(this.minecraft.font, "S", compassX + 27, compassY + 45, 0xFFFFFF); graphics.drawString(this.minecraft.font, "E", compassX + 45, compassY + 24, 0xFFFFFF); graphics.drawString(this.minecraft.font, "W", compassX + 9, compassY + 24, 0xFFFFFF); }
private void drawMiniMap(GuiGraphics graphics, Player player) { int mapSize = 64; int mapX = graphics.guiWidth() - mapSize - 10; int mapY = graphics.guiHeight() - mapSize - 10;
// Draw map background graphics.fill(mapX, mapY, mapX + mapSize, mapY + mapSize, 0x66000000); graphics.renderOutline(mapX, mapY, mapX + mapSize, mapY + mapSize, 0xFFFFFFFF);
// Draw player position on minimap int playerMapX = mapX + mapSize / 2; int playerMapY = mapY + mapSize / 2;
graphics.fill(playerMapX - 2, playerMapY - 2, playerMapX + 2, playerMapY + 2, 0xFFFF0000);
// Draw nearby terrain (simplified) drawMiniMapTerrain(graphics, player, mapX, mapY, mapSize); }
private void drawMiniMapTerrain(GuiGraphics graphics, Player player, int mapX, int mapY, int mapSize) { Level level = this.minecraft.level; BlockPos centerPos = player.blockPosition();
int radius = mapSize / 2 / 16; // blocks from center
for (int x = -radius; x <= radius; x++) { for (int z = -radius; z <= radius; z++) { BlockPos pos = centerPos.offset(x, 0, z); BlockState state = level.getBlockState(pos);
// Simple terrain representation int color = getTerrainColor(state); int screenX = mapX + mapSize / 2 + x * 16; int screenY = mapY + mapSize / 2 + z * 16;
if (screenX >= mapX && screenX < mapX + mapSize && screenY >= mapY && screenY < mapY + mapSize) { graphics.fill(screenX, screenY, screenX + 16, screenY + 16, color); } } } }
private int getTerrainColor(BlockState state) { if (state.isAir()) { return 0x00000000; // Transparent for air } else if (state.is(Blocks.GRASS_BLOCK)) { return 0xFF228B22; // Green for grass } else if (state.is(Blocks.STONE)) { return FF808080; // Gray for stone } else if (state.is(Blocks.WATER)) { return 0xFF0066CC; // Blue for water } return 0xFF666666; // Default gray }
private void drawCustomEffects(GuiGraphics graphics, Player player) { int effectsY = this.hudY + 80;
// Draw custom effect indicators if (player.hasEffect(MobEffects.NIGHT_VISION)) { graphics.drawString(this.minecraft.font, Component.literal("Night Vision Active").withStyle(ChatFormatting.AQUA), this.hudX, effectsY, 0xFFFFFF); effectsY += 12; }
if (player.isInWater()) { graphics.drawString(this.minecraft.font, Component.literal("Underwater").withStyle(ChatFormatting.BLUE), this.hudX, effectsY, 0xFFFFFF); effectsY += 12; }
// Draw custom power indicators for (int i = 0; i < player.getInventory().getContainerSize(); i++) { ItemStack stack = player.getInventory().getItem(i); if (isPowerItem(stack)) { Component powerText = Component.literal("Power: ") .append(Component.literal(getPowerLevel(stack)) .withStyle(ChatFormatting.YELLOW));
graphics.drawString(this.minecraft.font, powerText, this.hudX, effectsY, 0xFFFFFF); effectsY += 12; break; // Only show first power item } } }
private boolean isPowerItem(ItemStack stack) { return stack.getItem() == Items.DIAMOND || stack.getItem() == Items.EMERALD || stack.getItem() == Items.GOLD_INGOT; }
private String getPowerLevel(ItemStack stack) { if (stack.getItem() == Items.DIAMOND) { return "High"; } else if (stack.getItem() == Items.EMERALD) { return "Medium"; } else if (stack.getItem() == Items.GOLD_INGOT) { return "Low"; } return "Unknown"; }
// HUD control methods public void toggleHud() { this.showHud = !this.showHud; }
public void setHudPosition(int x, int y) { this.hudX = x; this.hudY = y; }}Event-Based HUD Integration
Section titled “Event-Based HUD Integration”// Example: Using render events for HUD integration@EventBusSubscriber(modid = CustomHudMod.MOD_ID, bus = EventBusSubscriber.Bus.FORGE)public class CustomHudEventHandler { private static final CustomHudOverlay customHud = new CustomHudOverlay();
@SubscribeEvent public static void onRenderGuiOverlay(RenderGuiLayerEvent.Post event) { if (event.getName() == RenderGuiLayerLayerElementType.ALL) { customHud.render(event.getGuiGraphics(), event.getDeltaTracker()); } }
@SubscribeEvent public static void onKeyPress(KeyEvent event) { // Toggle HUD with F6 key if (event.getKey() == InputConstants.KEY_F6) { customHud.toggleHud(); event.setCanceled(true); // Prevent other handlers } }}Advanced UI Techniques
Section titled “Advanced UI Techniques”Animated UI Elements
Section titled “Animated UI Elements”// Example: Animated widget with smooth transitionspublic class AnimatedWidget extends AbstractWidget { private float currentProgress = 0.0f; private float targetProgress = 0.0f; private long lastUpdateTime = 0; private static final float ANIMATION_SPEED = 0.1f;
public AnimatedWidget(int x, int y, int width, int height, Component message) { super(x, y, width, height, message); }
@Override protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { updateAnimation();
// Draw animated progress bar int barWidth = (int)(this.width * this.currentProgress);
// Smooth color transition based on progress int barColor = interpolateColor(0xFF5555, 0xFF55FF55, this.currentProgress);
graphics.fill(this.getX(), this.getY(), this.getX() + barWidth, this.getY() + this.height, barColor);
// Draw animated text float scale = 1.0f + (float)Math.sin(this.currentProgress * Math.PI * 2) * 0.1f;
graphics.pose().pushMatrix(); graphics.pose().translate(this.getX() + this.width / 2, this.getY() + this.height / 2, 0); graphics.pose().scale(scale, scale, 1.0f);
Component text = Component.literal(String.format("%.1f%%", this.currentProgress * 100)); graphics.drawString(this.font, text, -this.font.width(text) / 2, -this.font.lineHeight / 2, 0xFFFFFF);
graphics.pose().popMatrix(); }
private void updateAnimation() { long currentTime = System.currentTimeMillis(); long deltaTime = currentTime - this.lastUpdateTime;
if (deltaTime > 16) { // Limit to ~60 FPS // Smooth animation with easing float deltaProgress = this.targetProgress - this.currentProgress; this.currentProgress += deltaProgress * ANIMATION_SPEED;
this.lastUpdateTime = currentTime; } }
private int interpolateColor(int color1, int color2, float t) { t = Mth.clamp(t, 0.0f, 1.0f);
int r1 = (color1 >> 16) & 0xFF; int g1 = (color1 >> 8) & 0xFF; int b1 = color1 & 0xFF;
int r2 = (color2 >> 16) & 0xFF; int g2 = (color2 >> 8) & 0xFF; int b2 = color2 & 0xFF;
int r = (int)(r1 + (r2 - r1) * t); int g = (int)(g1 + (g2 - g1) * t); int b = (int)(b1 + (b2 - b1) * t);
return (r << 16) | (g << 8) | b; }
public void setTargetProgress(float progress) { this.targetProgress = Mth.clamp(progress, 0.0f, 1.0f); }}Responsive UI Layout
Section titled “Responsive UI Layout”// Example: Responsive UI that adapts to screen sizepublic class ResponsiveScreen extends Screen { private final Map<String, AbstractWidget> widgets = new HashMap<>(); private ScreenSize currentSize;
public ResponsiveScreen() { super(Component.literal("Responsive Interface")); }
@Override protected void init() { this.clearWidgets(); this.widgets.clear();
// Determine screen size category this.currentSize = categorizeScreenSize();
// Create layout based on screen size switch (this.currentSize) { case SMALL -> createSmallLayout(); case MEDIUM -> createMediumLayout(); case LARGE -> createLargeLayout(); } }
@Override public void resize(Minecraft minecraft, int width, int height) { super.resize(minecraft, width, height); this.init(); // Reinitialize layout }
private ScreenSize categorizeScreenSize() { if (this.width < 600 || this.height < 400) { return ScreenSize.SMALL; } else if (this.width < 1000 || this.height < 600) { return ScreenSize.MEDIUM; } else { return ScreenSize.LARGE; } }
private void createSmallLayout() { // Compact vertical layout for small screens this.widgets.put("mainButton", Button.builder(Component.literal("Main Action"), this::onMainAction) .pos(this.width / 2 - 50, this.height / 2 - 60) .size(100, 20) .build() );
this.widgets.put("settingsButton", Button.builder(Component.literal("Settings"), this::onSettings) .pos(this.width / 2 - 50, this.height / 2 - 20) .size(100, 20) .build() );
this.widgets.put("exitButton", Button.builder(Component.literal("Exit"), this::onClose) .pos(this.width / 2 - 50, this.height / 2 + 20) .size(100, 20) .build() ); }
private void createMediumLayout() { // Horizontal layout for medium screens this.widgets.put("mainButton", Button.builder(Component.literal("Main Action"), this::onMainAction) .pos(this.width / 2 - 160, this.height / 2 - 20) .size(100, 20) .build() );
this.widgets.put("settingsButton", Button.builder(Component.literal("Settings"), this::onSettings) .pos(this.width / 2 - 50, this.height / 2 - 20) .size(100, 20) .build() );
this.widgets.put("helpButton", Button.builder(Component.literal("Help"), this::onHelp) .pos(this.width / 2 + 60, this.height / 2 - 20) .size(100, 20) .build() );
this.widgets.put("exitButton", Button.builder(Component.literal("Exit"), this::onClose) .pos(this.width / 2 - 50, this.height / 2 + 20) .size(100, 20) .build() ); }
private void createLargeLayout() { // Grid layout for large screens int buttonWidth = 120; int buttonHeight = 30; int spacing = 20; int totalWidth = 3 * buttonWidth + 2 * spacing; int totalHeight = 3 * buttonHeight + 2 * spacing;
int startX = (this.width - totalWidth) / 2; int startY = (this.height - totalHeight) / 2;
this.widgets.put("mainButton", Button.builder(Component.literal("Main Action"), this::onMainAction) .pos(startX, startY) .size(buttonWidth, buttonHeight) .build() );
this.widgets.put("settingsButton", Button.builder(Component.literal("Settings"), this::onSettings) .pos(startX + buttonWidth + spacing, startY) .size(buttonWidth, buttonHeight) .build() );
this.widgets.put("helpButton", Button.builder(Component.literal("Help"), this::onHelp) .pos(startX + 2 * (buttonWidth + spacing), startY) .size(buttonWidth, buttonHeight) .build() );
this.widgets.put("toolsButton", Button.builder(Component.literal("Tools"), this::onTools) .pos(startX, startY + buttonHeight + spacing) .size(buttonWidth, buttonHeight) .build() );
this.widgets.put("statsButton", Button.builder(Component.literal("Statistics"), this::onStats) .pos(startX + buttonWidth + spacing, startY + buttonHeight + spacing) .size(buttonWidth, buttonHeight) .build() );
this.widgets.put("exitButton", Button.builder(Component.literal("Exit"), this::onClose) .pos(startX + 2 * (buttonWidth + spacing), startY + buttonHeight + spacing) .size(buttonWidth, buttonHeight) .build() ); }
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { this.renderBackground(graphics, mouseX, mouseY, partialTick);
// Draw screen info Component sizeText = Component.literal("Screen Size: ") .append(Component.literal(this.currentSize.toString()) .withStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW)));
graphics.drawString(this.font, sizeText, 10, 10, 0xFFFFFF);
// Render all widgets for (AbstractWidget widget : this.widgets.values()) { this.addRenderableWidget(widget); }
super.render(graphics, mouseX, mouseY, partialTick); }
private void onMainAction(Button button) { System.out.println("Main action triggered"); }
private void onSettings(Button button) { System.out.println("Settings triggered"); }
private void onHelp(Button button) { System.out.println("Help triggered"); }
private void onTools(Button button) { System.out.println("Tools triggered"); }
private void onStats(Button button) { System.out.println("Statistics triggered"); }
private enum ScreenSize { SMALL, MEDIUM, LARGE }}Input Handling
Section titled “Input Handling”Advanced Input Processing
Section titled “Advanced Input Processing”// Example: Complex input handling with keyboard shortcutspublic class AdvancedInputHandler { private final Map<Integer, Runnable> keyBindings = new HashMap<>(); private boolean shiftPressed = false; private boolean ctrlPressed = false; private boolean altPressed = false;
public AdvancedInputHandler() { setupKeyBindings(); }
private void setupKeyBindings() { // Register keyboard shortcuts this.keyBindings.put(InputConstants.KEY_S, this::saveAction); this.keyBindings.put(InputConstants.KEY_L, this::loadAction); this.keyBindings.put(InputConstants.KEY_O, this::openAction); this.keyBindings.put(InputConstants.KEY_N, this::newAction); this.keyBindings.put(InputConstants.KEY_DELETE, this::deleteAction); this.keyBindings.put(InputConstants.KEY_F, this::findAction); this.keyBindings.put(InputConstants.KEY_R, this::replaceAction); }
@SubscribeEvent public void onKeyPress(KeyEvent event) { updateModifierKeys(event);
// Check for modifier combinations if (this.ctrlPressed && this.shiftPressed) { if (event.getKey() == InputConstants.KEY_S) { saveAsAction(); event.setCanceled(true); return; } }
// Check for single key bindings Runnable action = this.keyBindings.get(event.getKey()); if (action != null && shouldHandleKey(event)) { action.run(); event.setCanceled(true); } }
@SubscribeEvent public void onKeyRelease(KeyEvent event) { updateModifierKeys(event); }
private void updateModifierKeys(KeyEvent event) { this.shiftPressed = event.getModifiers() == InputConstants.Modifiers.SHIFT; this.ctrlPressed = event.getModifiers() == InputConstants.Modifiers.CTRL; this.altPressed = event.getModifiers() == InputConstants.Modifiers.ALT; }
private boolean shouldHandleKey(KeyEvent event) { // Don't handle key presses when in chat or text fields Minecraft mc = Minecraft.getInstance(); if (mc.screen instanceof ChatScreen || (mc.screen != null && mc.screen.getFocused() instanceof TextFieldWidget)) { return false; } return true; }
// Action methods private void saveAction() { System.out.println("Save action triggered"); }
private void loadAction() { System.out.println("Load action triggered"); }
private void openAction() { System.out.println("Open action triggered"); }
private void newAction() { System.out.println("New action triggered"); }
private void deleteAction() { System.out.println("Delete action triggered"); }
private void findAction() { System.out.println("Find action triggered"); }
private void replaceAction() { System.out.println("Replace action triggered"); }
private void saveAsAction() { System.out.println("Save As action triggered"); }}Performance Optimization
Section titled “Performance Optimization”Efficient Rendering Techniques
Section titled “Efficient Rendering Techniques”// Example: Performance-optimized widgetpublic class EfficientWidget extends AbstractWidget { private final Font font; private String cachedText = ""; private int cachedColor = 0; private Component cachedComponent = null; private long lastUpdate = 0; private static final long CACHE_DURATION = 100; // ms
public EfficientWidget(int x, int y, int width, int height, Component message) { super(x, y, width, height, message); this.font = Minecraft.getInstance().font; }
@Override protected void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { // Avoid unnecessary updates long currentTime = System.currentTimeMillis(); if (currentTime - lastUpdate > CACHE_DURATION) { updateCachedValues(); lastUpdate = currentTime; }
// Use cached values for rendering if (cachedComponent != null) { graphics.drawString(this.font, cachedComponent, this.getX(), this.getY(), cachedColor); } }
private void updateCachedValues() { String newText = this.getMessage().getString(); int newColor = calculateDynamicColor();
// Only update if values changed if (!newText.equals(cachedText) || newColor != cachedColor) { cachedText = newText; cachedColor = newColor; cachedComponent = Component.literal(cachedText) .withStyle(Style.EMPTY.withColor(TextColor.fromRgb( (cachedColor >> 16) & 0xFF, (cachedColor >> 8) & 0xFF, cachedColor & 0xFF ))); } }
private int calculateDynamicColor() { // Example: Color changes based on time long time = System.currentTimeMillis() / 1000; float hue = (time % 10) / 10.0f; return Color.HSBtoRGB(hue, 0.7f, 0.9f); }}Common Issues and Solutions
Section titled “Common Issues and Solutions”Input Focus Problems
Section titled “Input Focus Problems”Problem: Custom widgets don’t receive input Solution: Ensure proper event handling chain and focus management
Rendering Order Issues
Section titled “Rendering Order Issues”Problem: Elements render in wrong order Solution: Use strata system and proper widget hierarchy
Performance Issues
Section titled “Performance Issues”Problem: Custom GUI causes lag Solution: Implement efficient caching and avoid per-frame object creation
Thread Safety Issues
Section titled “Thread Safety Issues”Problem: GUI operations cause crashes Solution: Ensure all GUI operations happen on main thread
Best Practices
Section titled “Best Practices”- Use efficient caching for expensive operations
- Implement proper event handling with correct propagation
- Design responsive layouts that adapt to different screen sizes
- Provide visual feedback for user interactions
- Handle accessibility concerns with proper contrast and sizing
- Test across different resolutions and UI scales
Next Steps
Section titled “Next Steps”- Multi-threaded Rendering - Understanding parallel rendering
- Performance Optimization - Advanced optimization techniques
- Advanced Shader Techniques - Complex shader effects
- Custom UI Examples - Complete working examples