diff --git a/net/minecraft/client/gui/screens/inventory/AbstractContainerScreen.java b/net/minecraft/client/gui/screens/inventory/AbstractContainerScreen.java new file mode 100644 index 0000000..b83b1a0 --- /dev/null +++ b/net/minecraft/client/gui/screens/inventory/AbstractContainerScreen.java @@ -0,0 +1,767 @@ +package net.minecraft.client.gui.screens.inventory; + +import com.google.common.collect.Sets; +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.datafixers.util.Pair; +import java.util.List; +import java.util.Set; +import javax.annotation.Nullable; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public abstract class AbstractContainerScreen extends Screen implements MenuAccess { + /** + * The location of the inventory background texture + */ + public static final ResourceLocation INVENTORY_LOCATION = ResourceLocation.withDefaultNamespace("textures/gui/container/inventory.png"); + private static final float SNAPBACK_SPEED = 100.0F; + private static final int QUICKDROP_DELAY = 500; + public static final int SLOT_ITEM_BLIT_OFFSET = 100; + private static final int HOVER_ITEM_BLIT_OFFSET = 200; + /** + * The X size of the inventory window in pixels. + */ + protected int imageWidth = 176; + /** + * The Y size of the inventory window in pixels. + */ + protected int imageHeight = 166; + protected int titleLabelX; + protected int titleLabelY; + protected int inventoryLabelX; + protected int inventoryLabelY; + /** + * A list of the players inventory slots + */ + protected final T menu; + protected final Component playerInventoryTitle; + /** + * Holds the slot currently hovered + */ + @Nullable + protected Slot hoveredSlot; + /** + * Used when touchscreen is enabled + */ + @Nullable + private Slot clickedSlot; + @Nullable + private Slot snapbackEnd; + @Nullable + private Slot quickdropSlot; + @Nullable + private Slot lastClickSlot; + /** + * Starting X position for the Gui. Inconsistent use for Gui backgrounds. + */ + protected int leftPos; + /** + * Starting Y position for the Gui. Inconsistent use for Gui backgrounds. + */ + protected int topPos; + /** + * Used when touchscreen is enabled. + */ + private boolean isSplittingStack; + /** + * Used when touchscreen is enabled + */ + private ItemStack draggingItem = ItemStack.EMPTY; + private int snapbackStartX; + private int snapbackStartY; + private long snapbackTime; + /** + * Used when touchscreen is enabled + */ + private ItemStack snapbackItem = ItemStack.EMPTY; + private long quickdropTime; + protected final Set quickCraftSlots = Sets.newHashSet(); + protected boolean isQuickCrafting; + private int quickCraftingType; + private int quickCraftingButton; + private boolean skipNextRelease; + private int quickCraftingRemainder; + private long lastClickTime; + private int lastClickButton; + private boolean doubleclick; + private ItemStack lastQuickMoved = ItemStack.EMPTY; + + public AbstractContainerScreen(T menu, Inventory playerInventory, Component title) { + super(title); + this.menu = menu; + this.playerInventoryTitle = playerInventory.getDisplayName(); + this.skipNextRelease = true; + this.titleLabelX = 8; + this.titleLabelY = 6; + this.inventoryLabelX = 8; + this.inventoryLabelY = this.imageHeight - 94; + } + + @Override + protected void init() { + this.leftPos = (this.width - this.imageWidth) / 2; + this.topPos = (this.height - this.imageHeight) / 2; + } + + /** + * Renders the graphical user interface (GUI) element. + * + * @param guiGraphics the GuiGraphics object used for rendering. + * @param mouseX the x-coordinate of the mouse cursor. + * @param mouseY the y-coordinate of the mouse cursor. + * @param partialTick the partial tick time. + */ + @Override + public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + int i = this.leftPos; + int j = this.topPos; + // Neo: replicate the super method's implementation to insert the event between background and widgets + this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + net.neoforged.neoforge.common.NeoForge.EVENT_BUS.post(new net.neoforged.neoforge.client.event.ContainerScreenEvent.Render.Background(this, guiGraphics, mouseX, mouseY)); + for (net.minecraft.client.gui.components.Renderable renderable : this.renderables) { + renderable.render(guiGraphics, mouseX, mouseY, partialTick); + } + RenderSystem.disableDepthTest(); + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate((float)i, (float)j, 0.0F); + this.hoveredSlot = null; + + for (int k = 0; k < this.menu.slots.size(); k++) { + Slot slot = this.menu.slots.get(k); + if (slot.isActive()) { + this.renderSlot(guiGraphics, slot); + } + + if (this.isHovering(slot, (double)mouseX, (double)mouseY) && slot.isActive()) { + this.hoveredSlot = slot; + this.renderSlotHighlight(guiGraphics, slot, mouseX, mouseY, partialTick); + } + } + + this.renderLabels(guiGraphics, mouseX, mouseY); + net.neoforged.neoforge.common.NeoForge.EVENT_BUS.post(new net.neoforged.neoforge.client.event.ContainerScreenEvent.Render.Foreground(this, guiGraphics, mouseX, mouseY)); + ItemStack itemstack = this.draggingItem.isEmpty() ? this.menu.getCarried() : this.draggingItem; + if (!itemstack.isEmpty()) { + int l1 = 8; + int i2 = this.draggingItem.isEmpty() ? 8 : 16; + String s = null; + if (!this.draggingItem.isEmpty() && this.isSplittingStack) { + itemstack = itemstack.copyWithCount(Mth.ceil((float)itemstack.getCount() / 2.0F)); + } else if (this.isQuickCrafting && this.quickCraftSlots.size() > 1) { + itemstack = itemstack.copyWithCount(this.quickCraftingRemainder); + if (itemstack.isEmpty()) { + s = ChatFormatting.YELLOW + "0"; + } + } + + this.renderFloatingItem(guiGraphics, itemstack, mouseX - i - 8, mouseY - j - i2, s); + } + + if (!this.snapbackItem.isEmpty()) { + float f = (float)(Util.getMillis() - this.snapbackTime) / 100.0F; + if (f >= 1.0F) { + f = 1.0F; + this.snapbackItem = ItemStack.EMPTY; + } + + int j2 = this.snapbackEnd.x - this.snapbackStartX; + int k2 = this.snapbackEnd.y - this.snapbackStartY; + int j1 = this.snapbackStartX + (int)((float)j2 * f); + int k1 = this.snapbackStartY + (int)((float)k2 * f); + this.renderFloatingItem(guiGraphics, this.snapbackItem, j1, k1, null); + } + + guiGraphics.pose().popPose(); + RenderSystem.enableDepthTest(); + } + + @Override + public void renderBackground(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + this.renderTransparentBackground(guiGraphics); + this.renderBg(guiGraphics, partialTick, mouseX, mouseY); + } + + public static void renderSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) { + renderSlotHighlight(guiGraphics, x, y, blitOffset, -2130706433); + } + public static void renderSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset, int color) { + guiGraphics.fillGradient(RenderType.guiOverlay(), x, y, x + 16, y + 16, color, color, blitOffset); + } + + /** + * Renders a highlight for the given slot to indicate the mouse is currently hovering over it. + */ + protected void renderSlotHighlight(GuiGraphics guiGraphics, Slot slot, int mouseX, int mouseY, float partialTick) { + if (slot.isHighlightable()) { + renderSlotHighlight(guiGraphics, slot.x, slot.y, 0, getSlotColor(slot.index)); + } + } + + protected void renderTooltip(GuiGraphics guiGraphics, int x, int y) { + if (this.menu.getCarried().isEmpty() && this.hoveredSlot != null && this.hoveredSlot.hasItem()) { + ItemStack itemstack = this.hoveredSlot.getItem(); + guiGraphics.renderTooltip(this.font, this.getTooltipFromContainerItem(itemstack), itemstack.getTooltipImage(), itemstack, x, y); + } + } + + protected List getTooltipFromContainerItem(ItemStack stack) { + return getTooltipFromItem(this.minecraft, stack); + } + + private void renderFloatingItem(GuiGraphics guiGraphics, ItemStack stack, int x, int y, String text) { + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0.0F, 0.0F, 232.0F); + guiGraphics.renderItem(stack, x, y); + var font = net.neoforged.neoforge.client.extensions.common.IClientItemExtensions.of(stack).getFont(stack, net.neoforged.neoforge.client.extensions.common.IClientItemExtensions.FontContext.ITEM_COUNT); + guiGraphics.renderItemDecorations(font == null ? this.font : font, stack, x, y - (this.draggingItem.isEmpty() ? 0 : 8), text); + guiGraphics.pose().popPose(); + } + + protected void renderLabels(GuiGraphics guiGraphics, int mouseX, int mouseY) { + guiGraphics.drawString(this.font, this.title, this.titleLabelX, this.titleLabelY, 4210752, false); + guiGraphics.drawString(this.font, this.playerInventoryTitle, this.inventoryLabelX, this.inventoryLabelY, 4210752, false); + } + + protected abstract void renderBg(GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY); + + protected void renderSlot(GuiGraphics guiGraphics, Slot slot) { + int i = slot.x; + int j = slot.y; + ItemStack itemstack = slot.getItem(); + boolean flag = false; + boolean flag1 = slot == this.clickedSlot && !this.draggingItem.isEmpty() && !this.isSplittingStack; + ItemStack itemstack1 = this.menu.getCarried(); + String s = null; + if (slot == this.clickedSlot && !this.draggingItem.isEmpty() && this.isSplittingStack && !itemstack.isEmpty()) { + itemstack = itemstack.copyWithCount(itemstack.getCount() / 2); + } else if (this.isQuickCrafting && this.quickCraftSlots.contains(slot) && !itemstack1.isEmpty()) { + if (this.quickCraftSlots.size() == 1) { + return; + } + + if (AbstractContainerMenu.canItemQuickReplace(slot, itemstack1, true) && this.menu.canDragTo(slot)) { + flag = true; + int k = Math.min(itemstack1.getMaxStackSize(), slot.getMaxStackSize(itemstack1)); + int l = slot.getItem().isEmpty() ? 0 : slot.getItem().getCount(); + int i1 = AbstractContainerMenu.getQuickCraftPlaceCount(this.quickCraftSlots, this.quickCraftingType, itemstack1) + l; + if (i1 > k) { + i1 = k; + s = ChatFormatting.YELLOW.toString() + k; + } + + itemstack = itemstack1.copyWithCount(i1); + } else { + this.quickCraftSlots.remove(slot); + this.recalculateQuickCraftRemaining(); + } + } + + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0.0F, 0.0F, 100.0F); + if (itemstack.isEmpty() && slot.isActive()) { + Pair pair = slot.getNoItemIcon(); + if (pair != null) { + TextureAtlasSprite textureatlassprite = this.minecraft.getTextureAtlas(pair.getFirst()).apply(pair.getSecond()); + guiGraphics.blit(i, j, 0, 16, 16, textureatlassprite); + flag1 = true; + } + } + + if (!flag1) { + if (flag) { + guiGraphics.fill(i, j, i + 16, j + 16, -2130706433); + } + + renderSlotContents(guiGraphics, itemstack, slot, s); + } + + guiGraphics.pose().popPose(); + } + + protected void renderSlotContents(GuiGraphics guiGraphics, ItemStack itemstack, Slot slot, @Nullable String countString) { + GuiGraphics p_281607_ = guiGraphics; Slot p_282613_ = slot; String s = countString; int i = slot.x; int j = slot.y; + int j1 = p_282613_.x + p_282613_.y * this.imageWidth; + if (p_282613_.isFake()) { + p_281607_.renderFakeItem(itemstack, i, j, j1); + } else { + p_281607_.renderItem(itemstack, i, j, j1); + } + + p_281607_.renderItemDecorations(this.font, itemstack, i, j, s); + } + + private void recalculateQuickCraftRemaining() { + ItemStack itemstack = this.menu.getCarried(); + if (!itemstack.isEmpty() && this.isQuickCrafting) { + if (this.quickCraftingType == 2) { + this.quickCraftingRemainder = itemstack.getMaxStackSize(); + } else { + this.quickCraftingRemainder = itemstack.getCount(); + + for (Slot slot : this.quickCraftSlots) { + ItemStack itemstack1 = slot.getItem(); + int i = itemstack1.isEmpty() ? 0 : itemstack1.getCount(); + int j = Math.min(itemstack.getMaxStackSize(), slot.getMaxStackSize(itemstack)); + int k = Math.min(AbstractContainerMenu.getQuickCraftPlaceCount(this.quickCraftSlots, this.quickCraftingType, itemstack) + i, j); + this.quickCraftingRemainder -= k - i; + } + } + } + } + + @Nullable + private Slot findSlot(double mouseX, double mouseY) { + for (int i = 0; i < this.menu.slots.size(); i++) { + Slot slot = this.menu.slots.get(i); + if (this.isHovering(slot, mouseX, mouseY) && slot.isActive()) { + return slot; + } + } + + return null; + } + + /** + * Called when a mouse button is clicked within the GUI element. + *

+ * @return {@code true} if the event is consumed, {@code false} otherwise. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button the button that was clicked. + */ + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)) { + return true; + } else { + InputConstants.Key mouseKey = InputConstants.Type.MOUSE.getOrCreate(button); + boolean flag = this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey); + Slot slot = this.findSlot(mouseX, mouseY); + long i = Util.getMillis(); + this.doubleclick = this.lastClickSlot == slot && i - this.lastClickTime < 250L && this.lastClickButton == button; + this.skipNextRelease = false; + if (button != 0 && button != 1 && !flag) { + this.checkHotbarMouseClicked(button); + } else { + int j = this.leftPos; + int k = this.topPos; + boolean flag1 = this.hasClickedOutside(mouseX, mouseY, j, k, button); + if (slot != null) flag1 = false; // Forge, prevent dropping of items through slots outside of GUI boundaries + int l = -1; + if (slot != null) { + l = slot.index; + } + + if (flag1) { + l = -999; + } + + if (this.minecraft.options.touchscreen().get() && flag1 && this.menu.getCarried().isEmpty()) { + this.onClose(); + return true; + } + + if (l != -1) { + if (this.minecraft.options.touchscreen().get()) { + if (slot != null && slot.hasItem()) { + this.clickedSlot = slot; + this.draggingItem = ItemStack.EMPTY; + this.isSplittingStack = button == 1; + } else { + this.clickedSlot = null; + } + } else if (!this.isQuickCrafting) { + if (this.menu.getCarried().isEmpty()) { + if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) { + this.slotClicked(slot, l, button, ClickType.CLONE); + } else { + boolean flag2 = l != -999 + && ( + InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 340) + || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 344) + ); + ClickType clicktype = ClickType.PICKUP; + if (flag2) { + this.lastQuickMoved = slot != null && slot.hasItem() ? slot.getItem().copy() : ItemStack.EMPTY; + clicktype = ClickType.QUICK_MOVE; + } else if (l == -999) { + clicktype = ClickType.THROW; + } + + this.slotClicked(slot, l, button, clicktype); + } + + this.skipNextRelease = true; + } else { + this.isQuickCrafting = true; + this.quickCraftingButton = button; + this.quickCraftSlots.clear(); + if (button == 0) { + this.quickCraftingType = 0; + } else if (button == 1) { + this.quickCraftingType = 1; + } else if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) { + this.quickCraftingType = 2; + } + } + } + } + } + + this.lastClickSlot = slot; + this.lastClickTime = i; + this.lastClickButton = button; + return true; + } + } + + private void checkHotbarMouseClicked(int keyCode) { + if (this.hoveredSlot != null && this.menu.getCarried().isEmpty()) { + if (this.minecraft.options.keySwapOffhand.matchesMouse(keyCode)) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 40, ClickType.SWAP); + return; + } + + for (int i = 0; i < 9; i++) { + if (this.minecraft.options.keyHotbarSlots[i].matchesMouse(keyCode)) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, i, ClickType.SWAP); + } + } + } + } + + protected boolean hasClickedOutside(double mouseX, double mouseY, int guiLeft, int guiTop, int mouseButton) { + return mouseX < (double)guiLeft + || mouseY < (double)guiTop + || mouseX >= (double)(guiLeft + this.imageWidth) + || mouseY >= (double)(guiTop + this.imageHeight); + } + + /** + * Called when the mouse is dragged within the GUI element. + *

+ * @return {@code true} if the event is consumed, {@code false} otherwise. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button the button that is being dragged. + * @param dragX the X distance of the drag. + * @param dragY the Y distance of the drag. + */ + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + Slot slot = this.findSlot(mouseX, mouseY); + ItemStack itemstack = this.menu.getCarried(); + if (this.clickedSlot != null && this.minecraft.options.touchscreen().get()) { + if (button == 0 || button == 1) { + if (this.draggingItem.isEmpty()) { + if (slot != this.clickedSlot && !this.clickedSlot.getItem().isEmpty()) { + this.draggingItem = this.clickedSlot.getItem().copy(); + } + } else if (this.draggingItem.getCount() > 1 && slot != null && AbstractContainerMenu.canItemQuickReplace(slot, this.draggingItem, false)) { + long i = Util.getMillis(); + if (this.quickdropSlot == slot) { + if (i - this.quickdropTime > 500L) { + this.slotClicked(this.clickedSlot, this.clickedSlot.index, 0, ClickType.PICKUP); + this.slotClicked(slot, slot.index, 1, ClickType.PICKUP); + this.slotClicked(this.clickedSlot, this.clickedSlot.index, 0, ClickType.PICKUP); + this.quickdropTime = i + 750L; + this.draggingItem.shrink(1); + } + } else { + this.quickdropSlot = slot; + this.quickdropTime = i; + } + } + } + } else if (this.isQuickCrafting + && slot != null + && !itemstack.isEmpty() + && (itemstack.getCount() > this.quickCraftSlots.size() || this.quickCraftingType == 2) + && AbstractContainerMenu.canItemQuickReplace(slot, itemstack, true) + && slot.mayPlace(itemstack) + && this.menu.canDragTo(slot)) { + this.quickCraftSlots.add(slot); + this.recalculateQuickCraftRemaining(); + } + + return true; + } + + /** + * Called when a mouse button is released within the GUI element. + *

+ * @return {@code true} if the event is consumed, {@code false} otherwise. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button the button that was released. + */ + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + super.mouseReleased(mouseX, mouseY, button); //Forge, Call parent to release buttons + Slot slot = this.findSlot(mouseX, mouseY); + int i = this.leftPos; + int j = this.topPos; + boolean flag = this.hasClickedOutside(mouseX, mouseY, i, j, button); + if (slot != null) flag = false; // Forge, prevent dropping of items through slots outside of GUI boundaries + InputConstants.Key mouseKey = InputConstants.Type.MOUSE.getOrCreate(button); + int k = -1; + if (slot != null) { + k = slot.index; + } + + if (flag) { + k = -999; + } + + if (this.doubleclick && slot != null && button == 0 && this.menu.canTakeItemForPickAll(ItemStack.EMPTY, slot)) { + if (hasShiftDown()) { + if (!this.lastQuickMoved.isEmpty()) { + for (Slot slot2 : this.menu.slots) { + if (slot2 != null + && slot2.mayPickup(this.minecraft.player) + && slot2.hasItem() + && slot2.isSameInventory(slot) + && AbstractContainerMenu.canItemQuickReplace(slot2, this.lastQuickMoved, true)) { + this.slotClicked(slot2, slot2.index, button, ClickType.QUICK_MOVE); + } + } + } + } else { + this.slotClicked(slot, k, button, ClickType.PICKUP_ALL); + } + + this.doubleclick = false; + this.lastClickTime = 0L; + } else { + if (this.isQuickCrafting && this.quickCraftingButton != button) { + this.isQuickCrafting = false; + this.quickCraftSlots.clear(); + this.skipNextRelease = true; + return true; + } + + if (this.skipNextRelease) { + this.skipNextRelease = false; + return true; + } + + if (this.clickedSlot != null && this.minecraft.options.touchscreen().get()) { + if (button == 0 || button == 1) { + if (this.draggingItem.isEmpty() && slot != this.clickedSlot) { + this.draggingItem = this.clickedSlot.getItem(); + } + + boolean flag2 = AbstractContainerMenu.canItemQuickReplace(slot, this.draggingItem, false); + if (k != -1 && !this.draggingItem.isEmpty() && flag2) { + this.slotClicked(this.clickedSlot, this.clickedSlot.index, button, ClickType.PICKUP); + this.slotClicked(slot, k, 0, ClickType.PICKUP); + if (this.menu.getCarried().isEmpty()) { + this.snapbackItem = ItemStack.EMPTY; + } else { + this.slotClicked(this.clickedSlot, this.clickedSlot.index, button, ClickType.PICKUP); + this.snapbackStartX = Mth.floor(mouseX - (double)i); + this.snapbackStartY = Mth.floor(mouseY - (double)j); + this.snapbackEnd = this.clickedSlot; + this.snapbackItem = this.draggingItem; + this.snapbackTime = Util.getMillis(); + } + } else if (!this.draggingItem.isEmpty()) { + this.snapbackStartX = Mth.floor(mouseX - (double)i); + this.snapbackStartY = Mth.floor(mouseY - (double)j); + this.snapbackEnd = this.clickedSlot; + this.snapbackItem = this.draggingItem; + this.snapbackTime = Util.getMillis(); + } + + this.clearDraggingState(); + } + } else if (this.isQuickCrafting && !this.quickCraftSlots.isEmpty()) { + this.slotClicked(null, -999, AbstractContainerMenu.getQuickcraftMask(0, this.quickCraftingType), ClickType.QUICK_CRAFT); + + for (Slot slot1 : this.quickCraftSlots) { + this.slotClicked(slot1, slot1.index, AbstractContainerMenu.getQuickcraftMask(1, this.quickCraftingType), ClickType.QUICK_CRAFT); + } + + this.slotClicked(null, -999, AbstractContainerMenu.getQuickcraftMask(2, this.quickCraftingType), ClickType.QUICK_CRAFT); + } else if (!this.menu.getCarried().isEmpty()) { + if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) { + this.slotClicked(slot, k, button, ClickType.CLONE); + } else { + boolean flag1 = k != -999 + && ( + InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 340) + || InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), 344) + ); + if (flag1) { + this.lastQuickMoved = slot != null && slot.hasItem() ? slot.getItem().copy() : ItemStack.EMPTY; + } + + this.slotClicked(slot, k, button, flag1 ? ClickType.QUICK_MOVE : ClickType.PICKUP); + } + } + } + + if (this.menu.getCarried().isEmpty()) { + this.lastClickTime = 0L; + } + + this.isQuickCrafting = false; + return true; + } + + public void clearDraggingState() { + this.draggingItem = ItemStack.EMPTY; + this.clickedSlot = null; + } + + private boolean isHovering(Slot slot, double mouseX, double mouseY) { + return this.isHovering(slot.x, slot.y, 16, 16, mouseX, mouseY); + } + + protected boolean isHovering(int x, int y, int width, int height, double mouseX, double mouseY) { + int i = this.leftPos; + int j = this.topPos; + mouseX -= (double)i; + mouseY -= (double)j; + return mouseX >= (double)(x - 1) + && mouseX < (double)(x + width + 1) + && mouseY >= (double)(y - 1) + && mouseY < (double)(y + height + 1); + } + + /** + * Called when the mouse is clicked over a slot or outside the gui. + */ + protected void slotClicked(Slot slot, int slotId, int mouseButton, ClickType type) { + if (slot != null) { + slotId = slot.index; + } + + this.minecraft.gameMode.handleInventoryMouseClick(this.menu.containerId, slotId, mouseButton, type, this.minecraft.player); + } + + protected void handleSlotStateChanged(int slotId, int containerId, boolean newState) { + this.minecraft.gameMode.handleSlotStateChanged(slotId, containerId, newState); + } + + /** + * Called when a keyboard key is pressed within the GUI element. + *

+ * @return {@code true} if the event is consumed, {@code false} otherwise. + * + * @param keyCode the key code of the pressed key. + * @param scanCode the scan code of the pressed key. + * @param modifiers the keyboard modifiers. + */ + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + InputConstants.Key mouseKey = InputConstants.getKey(keyCode, scanCode); + if (super.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } else if (this.minecraft.options.keyInventory.isActiveAndMatches(mouseKey)) { + this.onClose(); + return true; + } else { + boolean handled = this.checkHotbarKeyPressed(keyCode, scanCode);// Forge MC-146650: Needs to return true when the key is handled + if (this.hoveredSlot != null && this.hoveredSlot.hasItem()) { + if (this.minecraft.options.keyPickItem.isActiveAndMatches(mouseKey)) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 0, ClickType.CLONE); + handled = true; + } else if (this.minecraft.options.keyDrop.isActiveAndMatches(mouseKey)) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, hasControlDown() ? 1 : 0, ClickType.THROW); + handled = true; + } + } else if (this.minecraft.options.keyDrop.isActiveAndMatches(mouseKey)) { + handled = true; // Forge MC-146650: Emulate MC bug, so we don't drop from hotbar when pressing drop without hovering over a item. + } + + return handled; + } + } + + protected boolean checkHotbarKeyPressed(int keyCode, int scanCode) { + if (this.menu.getCarried().isEmpty() && this.hoveredSlot != null) { + if (this.minecraft.options.keySwapOffhand.isActiveAndMatches(InputConstants.getKey(keyCode, scanCode))) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, 40, ClickType.SWAP); + return true; + } + + for (int i = 0; i < 9; i++) { + if (this.minecraft.options.keyHotbarSlots[i].isActiveAndMatches(InputConstants.getKey(keyCode, scanCode))) { + this.slotClicked(this.hoveredSlot, this.hoveredSlot.index, i, ClickType.SWAP); + return true; + } + } + } + + return false; + } + + @Override + public void removed() { + if (this.minecraft.player != null) { + this.menu.removed(this.minecraft.player); + } + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public final void tick() { + super.tick(); + if (this.minecraft.player.isAlive() && !this.minecraft.player.isRemoved()) { + this.containerTick(); + } else { + this.minecraft.player.closeContainer(); + } + } + + protected void containerTick() { + } + + @Override + public T getMenu() { + return this.menu; + } + + @org.jetbrains.annotations.Nullable + public Slot getSlotUnderMouse() { return this.hoveredSlot; } + public int getGuiLeft() { return leftPos; } + public int getGuiTop() { return topPos; } + public int getXSize() { return imageWidth; } + public int getYSize() { return imageHeight; } + + protected int slotColor = -2130706433; + public int getSlotColor(int index) { + return slotColor; + } + + @Override + public void onClose() { + this.minecraft.player.closeContainer(); + super.onClose(); + } +} diff --git a/net/neoforged/neoforge/client/event/ScreenEvent.java b/net/neoforged/neoforge/client/event/ScreenEvent.java new file mode 100644 index 0000000..bb40c08 --- /dev/null +++ b/net/neoforged/neoforge/client/event/ScreenEvent.java @@ -0,0 +1,1031 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.event; + +import com.mojang.blaze3d.platform.InputConstants; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.EffectRenderingInventoryScreen; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.ICancellableEvent; +import net.neoforged.fml.LogicalSide; +import net.neoforged.neoforge.client.event.ScreenEvent.CharacterTyped; +import net.neoforged.neoforge.client.event.ScreenEvent.Init; +import net.neoforged.neoforge.client.event.ScreenEvent.KeyPressed; +import net.neoforged.neoforge.client.event.ScreenEvent.KeyReleased; +import net.neoforged.neoforge.client.event.ScreenEvent.MouseButtonPressed; +import net.neoforged.neoforge.client.event.ScreenEvent.MouseButtonReleased; +import net.neoforged.neoforge.client.event.ScreenEvent.MouseDragged; +import net.neoforged.neoforge.client.event.ScreenEvent.MouseScrolled; +import net.neoforged.neoforge.common.NeoForge; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +/** + * Fired on different events/actions when a {@link Screen} is active and visible. + * See the various subclasses for listening to different events. + * + *

These events are fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ * + * @see Init + * @see Render + * @see BackgroundRendered + * @see MouseInput + * @see KeyInput + */ +@OnlyIn(Dist.CLIENT) +public abstract class ScreenEvent extends Event { + private final Screen screen; + + @ApiStatus.Internal + protected ScreenEvent(Screen screen) { + this.screen = Objects.requireNonNull(screen); + } + + /** + * {@return the screen that caused this event} + */ + public Screen getScreen() { + return screen; + } + + /** + * Fired when a screen is being initialized. + * See the two subclasses for listening before and after the initialization. + * + *

Listeners added through this event may also be marked as renderable or narratable, if they inherit from + * {@link net.minecraft.client.gui.components.Renderable} and {@link net.minecraft.client.gui.narration.NarratableEntry} + * respectively.

+ * + * @see Init.Pre + * @see Init.Post + */ + public static abstract class Init extends ScreenEvent { + private final Consumer add; + private final Consumer remove; + + private final List listenerList; + + @ApiStatus.Internal + protected Init(Screen screen, List listenerList, Consumer add, Consumer remove) { + super(screen); + this.listenerList = Collections.unmodifiableList(listenerList); + this.add = add; + this.remove = remove; + } + + /** + * {@return unmodifiable view of list of event listeners on the screen} + */ + public List getListenersList() { + return listenerList; + } + + /** + * Adds the given {@link GuiEventListener} to the screen. + * + * @param listener the listener to add + */ + public void addListener(GuiEventListener listener) { + add.accept(listener); + } + + /** + * Removes the given {@link GuiEventListener} from the screen. + * + * @param listener the listener to remove + */ + public void removeListener(GuiEventListener listener) { + remove.accept(listener); + } + + /** + * Fired before the screen's overridable initialization method is fired. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the initialization method will not be called, and the widgets and children lists + * will not be cleared.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends Init implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, List list, Consumer add, Consumer remove) { + super(screen, list, add, remove); + } + } + + /** + * Fired after the screen's overridable initialization method is called. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends Init { + @ApiStatus.Internal + public Post(Screen screen, List list, Consumer add, Consumer remove) { + super(screen, list, add, remove); + } + } + } + + /** + * Fired when a screen is being drawn. + * See the two subclasses for listening before and after drawing. + * + * @see Render.Pre + * @see Render.Post + */ + public static abstract class Render extends ScreenEvent { + private final GuiGraphics guiGraphics; + private final int mouseX; + private final int mouseY; + private final float partialTick; + + @ApiStatus.Internal + protected Render(Screen screen, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super(screen); + this.guiGraphics = guiGraphics; + this.mouseX = mouseX; + this.mouseY = mouseY; + this.partialTick = partialTick; + } + + /** + * {@return the gui graphics used for rendering} + */ + public GuiGraphics getGuiGraphics() { + return guiGraphics; + } + + /** + * {@return the X coordinate of the mouse pointer} + */ + public int getMouseX() { + return mouseX; + } + + /** + * {@return the Y coordinate of the mouse pointer} + */ + public int getMouseY() { + return mouseY; + } + + /** + * {@return the partial tick} + */ + public float getPartialTick() { + return partialTick; + } + + /** + * Fired before the screen is drawn. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen will not be drawn.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends Render implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super(screen, guiGraphics, mouseX, mouseY, partialTick); + } + } + + /** + * Fired after the screen is drawn. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends Render { + @ApiStatus.Internal + public Post(Screen screen, GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { + super(screen, guiGraphics, mouseX, mouseY, partialTick); + } + } + } + + /** + * Fired directly after the background of the screen is drawn. (Only when the screen calls {@link Screen#renderBackground}) + * Can be used for drawing above the background but below the tooltips. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ * + * @deprecated Planned on being removed in 1.21.1 due to extreme maintenance burden to make this event fire consistently for most screens. + */ + @Deprecated(forRemoval = true, since = "21.0") + public static class BackgroundRendered extends ScreenEvent { + private final GuiGraphics guiGraphics; + + @ApiStatus.Internal + public BackgroundRendered(Screen screen, GuiGraphics guiGraphics) { + super(screen); + this.guiGraphics = guiGraphics; + } + + /** + * {@return the gui graphics used for rendering} + */ + public GuiGraphics getGuiGraphics() { + return guiGraphics; + } + } + + /** + * Fired ahead of rendering any active mob effects in the {@link EffectRenderingInventoryScreen inventory screen}. + * Can be used to select the size of the effects display (full or compact) or even hide or replace vanilla's rendering entirely. + * This event can also be used to modify the horizontal position of the stack of effects being rendered. + * + *

This event is {@linkplain ICancellableEvent cancellable} and does not {@linkplain HasResult have a result}. + * Cancelling this event will prevent vanilla rendering.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class RenderInventoryMobEffects extends ScreenEvent implements ICancellableEvent { + private final int availableSpace; + private boolean compact; + private int horizontalOffset; + + @ApiStatus.Internal + public RenderInventoryMobEffects(Screen screen, int availableSpace, boolean compact, int horizontalOffset) { + super(screen); + this.availableSpace = availableSpace; + this.compact = compact; + this.horizontalOffset = horizontalOffset; + } + + /** + * The available space to the right of the inventory. + */ + public int getAvailableSpace() { + return availableSpace; + } + + /** + * Whether the effects should be rendered in compact mode (only icons, no text), or the default full size. + */ + public boolean isCompact() { + return compact; + } + + /** + * The distance from the left side of the screen that the effect stack is rendered. Positive values shift this more to the right. + */ + public int getHorizontalOffset() { + return horizontalOffset; + } + + /** + * Replaces the horizontal offset of the effect stack + */ + public void setHorizontalOffset(int offset) { + horizontalOffset = offset; + } + + /** + * Adds to the horizontal offset of the effect stack. Negative values are acceptable. + */ + public void addHorizontalOffset(int offset) { + horizontalOffset += offset; + } + + /** + * Sets whether the effects should be rendered in compact mode (only icons, no text), or the default full size. + */ + public void setCompact(boolean compact) { + this.compact = compact; + } + } + + /** + * Fired whenever an action is performed by the mouse. + * See the various subclasses to listen for different actions. + * + * @see MouseButtonPressed + * @see MouseButtonReleased + * @see MouseDragged + * @see MouseScrolled + */ + private static abstract class MouseInput extends ScreenEvent { + private final double mouseX; + private final double mouseY; + + @ApiStatus.Internal + protected MouseInput(Screen screen, double mouseX, double mouseY) { + super(screen); + this.mouseX = mouseX; + this.mouseY = mouseY; + } + + /** + * {@return the X position of the mouse cursor, relative to the screen} + */ + public double getMouseX() { + return mouseX; + } + + /** + * {@return the Y position of the mouse cursor, relative to the screen} + */ + public double getMouseY() { + return mouseY; + } + } + + /** + * Fired when a mouse button is pressed. + * See the two subclasses for listening before and after the normal handling. + * + * @see MouseButtonPressed.Pre + * @see MouseButtonPressed.Post + */ + public static abstract class MouseButtonPressed extends MouseInput { + private final int button; + + @ApiStatus.Internal + public MouseButtonPressed(Screen screen, double mouseX, double mouseY, int button) { + super(screen, mouseX, mouseY); + this.button = button; + } + + /** + * {@return the mouse button's input code} + * + * @see GLFW mouse constants starting with 'GLFW_MOUSE_BUTTON_' + * @see the online GLFW documentation + */ + public int getButton() { + return button; + } + + /** + * Fired before the mouse click is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's mouse click handler will be bypassed + * and the corresponding {@link MouseButtonPressed.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends MouseButtonPressed implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, double mouseX, double mouseY, int button) { + super(screen, mouseX, mouseY, button); + } + } + + /** + * This event is fired after the mouse click is handled, if the corresponding {@link MouseButtonPressed.Pre} was not + * cancelled. + *

+ * It is only fired on the {@linkplain Dist#CLIENT physical client}. + */ + public static class Post extends MouseButtonPressed { + private final boolean handled; + private Result result = Result.DEFAULT; + + @ApiStatus.Internal + public Post(Screen screen, double mouseX, double mouseY, int button, boolean handled) { + super(screen, mouseX, mouseY, button); + this.handled = handled; + } + + /** + * {@return true if the mouse click was already handled by its screen} + */ + public boolean wasClickHandled() { + return this.handled; + } + + /** + * Changes the result of this event. + * + * @see {@link Result} for the possible states. + */ + public void setResult(Result result) { + this.result = result; + } + + /** + * {@return the result of this event, which controls if the click will be treated as handled} + */ + public Result getResult() { + return this.result; + } + + /** + * {@return The (possibly event-modified) state of the click} + */ + public boolean getClickResult() { + if (this.result == Result.FORCE_HANDLED) { + return true; + } + return this.result == Result.DEFAULT && this.wasClickHandled(); + } + + public static enum Result { + /** + * Forces the event to mark the click as handled by the screen. + */ + FORCE_HANDLED, + + /** + * The result of {@link Screen#mouseClicked(double, double, int)} will be used to determine if the click was handled. + * + * @see {@link Post#wasClickHandled()} + */ + DEFAULT, + + /** + * Forces the event to mark the click as not handled by the screen. + */ + FORCE_UNHANDLED; + } + } + } + + /** + * Fired when a mouse button is released. + * See the two subclasses for listening before and after the normal handling. + * + * @see MouseButtonReleased.Pre + * @see MouseButtonReleased.Post + */ + public static abstract class MouseButtonReleased extends MouseInput { + private final int button; + + @ApiStatus.Internal + public MouseButtonReleased(Screen screen, double mouseX, double mouseY, int button) { + super(screen, mouseX, mouseY); + this.button = button; + } + + /** + * {@return the mouse button's input code} + * + * @see GLFW mouse constants starting with 'GLFW_MOUSE_BUTTON_' + * @see the online GLFW documentation + */ + public int getButton() { + return button; + } + + /** + * Fired before the mouse release is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's mouse release handler will be bypassed + * and the corresponding {@link MouseButtonReleased.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends MouseButtonReleased implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, double mouseX, double mouseY, int button) { + super(screen, mouseX, mouseY, button); + } + } + + /** + * This event is fired after the mouse release is handled, if the corresponding {@link MouseButtonReleased.Pre} was + * not cancelled. + *

+ * It is only fired on the {@linkplain Dist#CLIENT physical client}. + */ + public static class Post extends MouseButtonReleased { + private final boolean handled; + private Result result = Result.DEFAULT; + + @ApiStatus.Internal + public Post(Screen screen, double mouseX, double mouseY, int button, boolean handled) { + super(screen, mouseX, mouseY, button); + this.handled = handled; + } + + /** + * @return {@code true} if the mouse release was already handled by the screen + */ + public boolean wasReleaseHandled() { + return handled; + } + + /** + * Changes the result of this event. + * + * @see {@link Result} for the possible states. + */ + public void setResult(Result result) { + this.result = result; + } + + /** + * {@return the result of this event, which controls if the release will be treated as handled} + */ + public Result getResult() { + return this.result; + } + + /** + * {@return The (possibly event-modified) state of the release} + */ + public boolean getReleaseResult() { + if (this.result == Result.FORCE_HANDLED) { + return true; + } + return this.result == Result.DEFAULT && this.wasReleaseHandled(); + } + + public static enum Result { + /** + * Forces the event to mark the release as handled by the screen. + */ + FORCE_HANDLED, + + /** + * The result of {@link Screen#mouseReleased(double, double, int)} will be used to determine if the click was handled. + * + * @see {@link Post#wasReleaseHandled()} + */ + DEFAULT, + + /** + * Forces the event to mark the release as not handled by the screen. + */ + FORCE_UNHANDLED; + } + } + } + + /** + * Fired when the mouse was dragged while a button is being held down. + * See the two subclasses for listening before and after the normal handling. + * + * @see MouseDragged.Pre + * @see MouseDragged.Post + */ + public static abstract class MouseDragged extends MouseInput { + private final int mouseButton; + private final double dragX; + private final double dragY; + + @ApiStatus.Internal + public MouseDragged(Screen screen, double mouseX, double mouseY, int mouseButton, double dragX, double dragY) { + super(screen, mouseX, mouseY); + this.mouseButton = mouseButton; + this.dragX = dragX; + this.dragY = dragY; + } + + /** + * {@return the mouse button's input code} + * + * @see GLFW mouse constants starting with 'GLFW_MOUSE_BUTTON_' + * @see the online GLFW documentation + */ + public int getMouseButton() { + return mouseButton; + } + + /** + * {@return amount of mouse drag along the X axis} + */ + public double getDragX() { + return dragX; + } + + /** + * {@return amount of mouse drag along the Y axis} + */ + public double getDragY() { + return dragY; + } + + /** + * Fired before the mouse drag is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's mouse drag handler will be bypassed + * and the corresponding {@link MouseDragged.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends MouseDragged implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, double mouseX, double mouseY, int mouseButton, double dragX, double dragY) { + super(screen, mouseX, mouseY, mouseButton, dragX, dragY); + } + } + + /** + * Fired after the mouse drag is handled, if not handled by the screen + * and the corresponding {@link MouseDragged.Pre} is not cancelled. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the mouse drag will be set as handled.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends MouseDragged { + @ApiStatus.Internal + public Post(Screen screen, double mouseX, double mouseY, int mouseButton, double dragX, double dragY) { + super(screen, mouseX, mouseY, mouseButton, dragX, dragY); + } + } + } + + /** + * Fired when the mouse was dragged while a button is being held down. + * See the two subclasses for listening before and after the normal handling. + * + * @see MouseScrolled.Pre + * @see MouseScrolled.Post + */ + public static abstract class MouseScrolled extends MouseInput { + private final double scrollDeltaX; + private final double scrollDeltaY; + + @ApiStatus.Internal + public MouseScrolled(Screen screen, double mouseX, double mouseY, double scrollDeltaX, double scrollDeltaY) { + super(screen, mouseX, mouseY); + this.scrollDeltaX = scrollDeltaX; + this.scrollDeltaY = scrollDeltaY; + } + + /** + * {@return the amount of change / delta of the mouse scroll on the X axis} + */ + public double getScrollDeltaX() { + return scrollDeltaX; + } + + /** + * {@return the amount of change / delta of the mouse scroll on the Y axis} + */ + public double getScrollDeltaY() { + return scrollDeltaY; + } + + /** + * Fired before the mouse scroll is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's mouse scroll handler will be bypassed + * and the corresponding {@link MouseScrolled.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends MouseScrolled implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, double mouseX, double mouseY, double scrollDeltaX, double scrollDeltaY) { + super(screen, mouseX, mouseY, scrollDeltaX, scrollDeltaY); + } + } + + /** + * Fired after the mouse scroll is handled, if not handled by the screen + * and the corresponding {@link MouseScrolled.Pre} is not cancelled. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the mouse scroll will be set as handled.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends MouseScrolled { + @ApiStatus.Internal + public Post(Screen screen, double mouseX, double mouseY, double scrollDeltaX, double scrollDeltaY) { + super(screen, mouseX, mouseY, scrollDeltaX, scrollDeltaY); + } + } + } + + /** + *

Fired whenever a keyboard key is pressed or released. + * See the various subclasses to listen for key pressing or releasing.

+ * + * @see KeyPressed + * @see KeyReleased + * @see InputConstants + * @see the online GLFW documentation + */ + private static abstract class KeyInput extends ScreenEvent { + private final int keyCode; + private final int scanCode; + private final int modifiers; + + @ApiStatus.Internal + protected KeyInput(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen); + this.keyCode = keyCode; + this.scanCode = scanCode; + this.modifiers = modifiers; + } + + /** + * {@return the {@code GLFW} (platform-agnostic) key code} + * + * @see InputConstants input constants starting with {@code KEY_} + * @see GLFW key constants starting with {@code GLFW_KEY_} + * @see the online GLFW documentation + */ + public int getKeyCode() { + return keyCode; + } + + /** + * {@return the platform-specific scan code} + *

+ * The scan code is unique for every key, regardless of whether it has a key code. + * Scan codes are platform-specific but consistent over time, so keys will have different scan codes depending + * on the platform but they are safe to save to disk as custom key bindings. + * + * @see InputConstants#getKey(int, int) + */ + public int getScanCode() { + return scanCode; + } + + /** + * {@return a bit field representing the active modifier keys} + * + * @see InputConstants#MOD_CONTROL CTRL modifier key bit + * @see GLFW#GLFW_MOD_SHIFT SHIFT modifier key bit + * @see GLFW#GLFW_MOD_ALT ALT modifier key bit + * @see GLFW#GLFW_MOD_SUPER SUPER modifier key bit + * @see GLFW#GLFW_KEY_CAPS_LOCK CAPS LOCK modifier key bit + * @see GLFW#GLFW_KEY_NUM_LOCK NUM LOCK modifier key bit + * @see the online GLFW documentation + */ + public int getModifiers() { + return modifiers; + } + } + + /** + * Fired when a keyboard key is pressed. + * See the two subclasses for listening before and after the normal handling. + * + * @see KeyPressed.Pre + * @see KeyPressed.Post + */ + public static abstract class KeyPressed extends KeyInput { + @ApiStatus.Internal + public KeyPressed(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + + /** + * Fired before the key press is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable} and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's key press handler will be bypassed + * and the corresponding {@link KeyPressed.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends KeyPressed implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + } + + /** + * Fired after the key press is handled, if not handled by the screen + * and the corresponding {@link KeyPressed.Pre} is not cancelled. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the key press will be set as handled.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends KeyPressed implements ICancellableEvent { + @ApiStatus.Internal + public Post(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + } + } + + /** + * Fired when a keyboard key is released. + * See the two subclasses for listening before and after the normal handling. + * + * @see KeyReleased.Pre + * @see KeyReleased.Post + */ + public static abstract class KeyReleased extends KeyInput { + @ApiStatus.Internal + public KeyReleased(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + + /** + * Fired before the key release is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's key release handler will be bypassed + * and the corresponding {@link KeyReleased.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends KeyReleased implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + } + + /** + * Fired after the key release is handled, if not handled by the screen + * and the corresponding {@link KeyReleased.Pre} is not cancelled. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the key release will be set as handled.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends KeyReleased implements ICancellableEvent { + @ApiStatus.Internal + public Post(Screen screen, int keyCode, int scanCode, int modifiers) { + super(screen, keyCode, scanCode, modifiers); + } + } + } + + /** + * Fired when a keyboard key corresponding to a character is typed. + * See the two subclasses for listening before and after the normal handling. + * + * @see CharacterTyped.Pre + * @see CharacterTyped.Post + * @see the online GLFW documentation + */ + public static abstract class CharacterTyped extends ScreenEvent { + private final char codePoint; + private final int modifiers; + + @ApiStatus.Internal + public CharacterTyped(Screen screen, char codePoint, int modifiers) { + super(screen); + this.codePoint = codePoint; + this.modifiers = modifiers; + } + + /** + * {@return the character code point} + */ + public char getCodePoint() { + return codePoint; + } + + /** + * {@return a bit field representing the active modifier keys} + * + * @see InputConstants#MOD_CONTROL CTRL modifier key bit + * @see GLFW#GLFW_MOD_SHIFT SHIFT modifier key bit + * @see GLFW#GLFW_MOD_ALT ALT modifier key bit + * @see GLFW#GLFW_MOD_SUPER SUPER modifier key bit + * @see GLFW#GLFW_KEY_CAPS_LOCK CAPS LOCK modifier key bit + * @see GLFW#GLFW_KEY_NUM_LOCK NUM LOCK modifier key bit + * @see the online GLFW documentation + */ + public int getModifiers() { + return modifiers; + } + + /** + * Fired before the character input is handled by the screen. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the screen's character input handler will be bypassed + * and the corresponding {@link CharacterTyped.Post} will not be fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Pre extends CharacterTyped implements ICancellableEvent { + @ApiStatus.Internal + public Pre(Screen screen, char codePoint, int modifiers) { + super(screen, codePoint, modifiers); + } + } + + /** + * Fired after the character input is handled, if not handled by the screen + * and the corresponding {@link CharacterTyped.Pre} is not cancelled. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If the event is cancelled, the character input will be set as handled.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Post extends CharacterTyped { + @ApiStatus.Internal + public Post(Screen screen, char codePoint, int modifiers) { + super(screen, codePoint, modifiers); + } + } + } + + /** + * Fired before any {@link Screen} is opened, to allow changing it or preventing it from being opened. + * All screen layers on the screen are closed before this event is fired. + * + *

This event is {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}. + * If this event is cancelled, then the {@code Screen} shall be prevented from opening and any previous screen + * will remain open. However, cancelling this event will not prevent the closing of screen layers which happened before + * this event fired.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Opening extends ScreenEvent implements ICancellableEvent { + @Nullable + private final Screen currentScreen; + private Screen newScreen; + + @ApiStatus.Internal + public Opening(@Nullable Screen currentScreen, Screen screen) { + super(screen); + this.currentScreen = currentScreen; + this.newScreen = screen; + } + + /** + * Gets the currently open screen at the time of the event being fired. + *

+ * May be null if no screen was open. + */ + @Nullable + public Screen getCurrentScreen() { + return currentScreen; + } + + /** + * @return The screen that will be opened if the event is not cancelled. May be null. + */ + @Nullable + public Screen getNewScreen() { + return newScreen; + } + + /** + * Sets the new screen to be opened if the event is not cancelled. May be null. + */ + public void setNewScreen(Screen newScreen) { + this.newScreen = newScreen; + } + } + + /** + * Fired before a {@link Screen} is closed. + * All screen layers on the screen are closed before this event is fired. + * + *

This event is not {@linkplain ICancellableEvent cancellable}, and does not {@linkplain HasResult have a result}.

+ * + *

This event is fired on the {@linkplain NeoForge#EVENT_BUS main Forge event bus}, + * only on the {@linkplain LogicalSide#CLIENT logical client}.

+ */ + public static class Closing extends ScreenEvent { + @ApiStatus.Internal + public Closing(Screen screen) { + super(screen); + } + } +} diff --git a/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java b/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java index 5af9459..c1e79c6 100644 --- a/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java +++ b/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java @@ -33,6 +33,7 @@ public class BagTabsClient { modEventBus.addListener(BagTabsClient::registerScreens); modEventBus.addListener(BagTabsClient::registerItemColors); modEventBus.addListener(BagTabsClient::registerKeyMappings); + NeoForge.EVENT_BUS.addListener(BagTabsClient::renderTabsPre); NeoForge.EVENT_BUS.addListener(BagTabsClient::renderTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::clickTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::dragTabs); @@ -56,6 +57,10 @@ public class BagTabsClient { event.register(OPEN_LAST_BAG); } + private static void renderTabsPre(ScreenEvent.Render.Pre event) { + BagTabOverlay.renderPre(event); + } + private static void renderTabs(ScreenEvent.Render.Post event) { BagTabOverlay.render(event); } diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index 4a2414d..aa204e7 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -48,6 +48,7 @@ public final class BagTabOverlay { private static final ResourceLocation GEAR_ICON_TEXTURE = BagTabs.id("textures/gui/dock_gear.png"); private static final ResourceLocation PREV_ICON_TEXTURE = BagTabs.id("textures/gui/dock_prev.png"); private static final ResourceLocation NEXT_ICON_TEXTURE = BagTabs.id("textures/gui/dock_next.png"); + private static final ResourceLocation POPUP_TEXTURE = BagTabs.id("textures/gui/dock_config_popup.png"); private static final int BASE_TAB_WIDTH = 22; private static final int BASE_TAB_HEIGHT = 22; private static final int BASE_COMPACT_TAB_WIDTH = 22; @@ -55,6 +56,9 @@ public final class BagTabOverlay { private static final int BASE_DOCK_WIDTH = 18; private static final int BASE_DOCK_HEIGHT = 22; private static final int BASE_DOCK_ICON_SIZE = 32; + private static final int POPUP_WIDTH = 140; + private static final int POPUP_HEIGHT = 160; + private static final int POPUP_TITLE_HEIGHT = 16; private static final int TAB_GAP = 0; private static final int TAB_Y_OFFSET = -3; private static final int TAB_X_OFFSET = -6; @@ -69,10 +73,38 @@ public final class BagTabOverlay { private static long nextAutoScrollAt; private static String lastOpenedBagKey; private static int lastOpenedBagSlot = -1; + private static ConfigPopupState configPopup; + private static boolean suppressUnderlyingHoverRender; + private static int overlayMouseX = Integer.MIN_VALUE; + private static int overlayMouseY = Integer.MIN_VALUE; private BagTabOverlay() { } + public static void renderPre(ScreenEvent.Render.Pre event) { + if (suppressUnderlyingHoverRender) { + return; + } + if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen)) { + return; + } + if (!isConfigPopupOpenFor(screen) || !isPointInsideConfigPopup(event.getMouseX(), event.getMouseY())) { + return; + } + + suppressUnderlyingHoverRender = true; + overlayMouseX = event.getMouseX(); + overlayMouseY = event.getMouseY(); + event.setCanceled(true); + try { + screen.render(event.getGuiGraphics(), -10_000, -10_000, event.getPartialTick()); + } finally { + suppressUnderlyingHoverRender = false; + overlayMouseX = Integer.MIN_VALUE; + overlayMouseY = Integer.MIN_VALUE; + } + } + public static void render(ScreenEvent.Render.Post event) { expirePendingCursorRestore(); if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen)) { @@ -89,8 +121,8 @@ public final class BagTabOverlay { } GuiGraphics g = event.getGuiGraphics(); - int mouseX = event.getMouseX(); - int mouseY = event.getMouseY(); + int mouseX = effectiveMouseX(event.getMouseX()); + int mouseY = effectiveMouseY(event.getMouseY()); ItemStack carried = screen.getMenu().getCarried(); refreshInsertTargets(screen, carried); @@ -133,10 +165,18 @@ public final class BagTabOverlay { g.pose().popPose(); } + if (isConfigPopupOpenFor(screen)) { + renderConfigPopup(g, screen, mouseX, mouseY); + } + if (dragState != null) { return; } + if (isConfigPopupOpenFor(screen) && isPointInsideConfigPopup(mouseX, mouseY)) { + return; + } + DockIcon hovered = strip.control().hoveredIcon(mouseX, mouseY); if (hovered != null) { g.renderTooltip(Minecraft.getInstance().font, List.of(getDockTooltip(hovered).getVisualOrderText()), mouseX, mouseY); @@ -168,6 +208,10 @@ public final class BagTabOverlay { } TabStrip strip = getTabStrip(screen, player); + if (handleConfigPopupClick(screen, event.getMouseX(), event.getMouseY(), event.getButton())) { + event.setCanceled(true); + return; + } if (event.getButton() == 0) { DockIcon clicked = strip.control().hoveredIcon(event.getMouseX(), event.getMouseY()); if (clicked == DockIcon.LOCK) { @@ -179,7 +223,7 @@ public final class BagTabOverlay { return; } if (clicked == DockIcon.CONFIG) { - Minecraft.getInstance().setScreen(new DockConfigScreen(screen)); + toggleConfigPopup(screen); Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); event.setCanceled(true); return; @@ -237,6 +281,11 @@ public final class BagTabOverlay { return; } + if (handleConfigPopupDrag(screen, event.getMouseX(), event.getMouseY())) { + event.setCanceled(true); + return; + } + TabStrip strip = getTabStrip(screen, player); List tabs = strip.tabs(); if (pendingClick != null && pendingClick.pinned()) { @@ -266,6 +315,11 @@ public final class BagTabOverlay { return; } + if (isConfigPopupOpenFor(screen) && isPointInsideConfigPopup(event.getMouseX(), event.getMouseY())) { + event.setCanceled(true); + return; + } + TabStrip strip = getTabStrip(screen, player); if (strip.scrollControl() == null || !isScrollHoverTarget(strip, event.getMouseX(), event.getMouseY())) { return; @@ -292,6 +346,11 @@ public final class BagTabOverlay { return; } + if (handleConfigPopupRelease(screen, event.getButton())) { + event.setCanceled(true); + return; + } + ItemStack carried = screen.getMenu().getCarried(); if (carried.isEmpty()) { TabStrip strip = getTabStrip(screen, player); @@ -607,6 +666,244 @@ public final class BagTabOverlay { return false; } + private static void toggleConfigPopup(AbstractContainerScreen screen) { + if (isConfigPopupOpenFor(screen)) { + configPopup = null; + return; + } + String screenKey = getScreenKey(screen); + int popupX = clamp((screen.width - POPUP_WIDTH) / 2, 4, Math.max(4, screen.width - POPUP_WIDTH - 4)); + int popupY = clamp((screen.height - POPUP_HEIGHT) / 2, 4, Math.max(4, screen.height - POPUP_HEIGHT - 4)); + configPopup = new ConfigPopupState( + screenKey, + DockConfigManager.hasOverride(screenKey), + DockConfigManager.getEditableSettings(screenKey, DockConfigManager.hasOverride(screenKey)).copy(), + popupX, + popupY, + false, + 0, + 0 + ); + } + + private static boolean isConfigPopupOpenFor(AbstractContainerScreen screen) { + return configPopup != null && configPopup.screenKey().equals(getScreenKey(screen)); + } + + private static boolean handleConfigPopupClick(AbstractContainerScreen screen, double mouseX, double mouseY, int button) { + if (!isConfigPopupOpenFor(screen)) { + return false; + } + if (!isPointInsideConfigPopup(mouseX, mouseY)) { + return false; + } + if (button == 0 && isPointInsideConfigPopupTitle(mouseX, mouseY)) { + configPopup = configPopup.withDragging(true, (int) mouseX - configPopup.x(), (int) mouseY - configPopup.y()); + return true; + } + + PopupElement element = getPopupElementAt(mouseX, mouseY); + if (element == null) { + return true; + } + if (button == 1 && element == PopupElement.SIDE) { + mutateConfigPopup(settings -> settings.withDockSide(settings.dockSide().previous())); + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + return true; + } + if (button != 0) { + return true; + } + + switch (element) { + case TARGET -> { + saveConfigPopup(); + boolean editOverride = !configPopup.editOverride(); + DockConfigManager.DockSettings settings = DockConfigManager.getEditableSettings(configPopup.screenKey(), editOverride).copy(); + configPopup = configPopup.withMode(editOverride, settings); + } + case SIDE -> mutateConfigPopup(settings -> settings.withDockSide(settings.dockSide().next())); + case X_MINUS -> mutateConfigPopup(settings -> settings.withXOffset(settings.xOffset() - getOffsetStep())); + case X_PLUS -> mutateConfigPopup(settings -> settings.withXOffset(settings.xOffset() + getOffsetStep())); + case Y_MINUS -> mutateConfigPopup(settings -> settings.withYOffset(settings.yOffset() - getOffsetStep())); + case Y_PLUS -> mutateConfigPopup(settings -> settings.withYOffset(settings.yOffset() + getOffsetStep())); + case MAX_MINUS -> mutateConfigPopup(settings -> settings.withMaxTabs(settings.maxTabs() - 1)); + case MAX_PLUS -> mutateConfigPopup(settings -> settings.withMaxTabs(settings.maxTabs() + 1)); + case COMPACT -> mutateConfigPopup(settings -> settings.withCompact(!settings.compact())); + case RESET -> { + DockConfigManager.clearOverride(configPopup.screenKey()); + configPopup = configPopup.withMode(false, DockConfigManager.getEditableSettings(configPopup.screenKey(), false).copy()); + } + case DONE -> { + saveConfigPopup(); + configPopup = null; + } + } + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + return true; + } + + private static boolean handleConfigPopupDrag(AbstractContainerScreen screen, double mouseX, double mouseY) { + if (!isConfigPopupOpenFor(screen) || !configPopup.dragging()) { + return false; + } + int x = clamp((int) mouseX - configPopup.dragOffsetX(), 4, Math.max(4, screen.width - POPUP_WIDTH - 4)); + int y = clamp((int) mouseY - configPopup.dragOffsetY(), 4, Math.max(4, screen.height - POPUP_HEIGHT - 4)); + configPopup = configPopup.withPosition(x, y); + return true; + } + + private static boolean handleConfigPopupRelease(AbstractContainerScreen screen, int button) { + if (!isConfigPopupOpenFor(screen) || button != 0 || !configPopup.dragging()) { + return false; + } + configPopup = configPopup.withDragging(false, 0, 0); + return true; + } + + private static void renderConfigPopup(GuiGraphics g, AbstractContainerScreen screen, int mouseX, int mouseY) { + if (!isConfigPopupOpenFor(screen)) { + return; + } + int x = configPopup.x(); + int y = configPopup.y(); + g.pose().pushPose(); + g.pose().translate(0.0F, 0.0F, 300.0F); + g.blit(POPUP_TEXTURE, x, y, 0, 0, POPUP_WIDTH, POPUP_HEIGHT, POPUP_WIDTH, POPUP_HEIGHT); + g.drawCenteredString(Minecraft.getInstance().font, BagTabs.translation("dock.title"), x + (POPUP_WIDTH / 2), y + 4, 0xFFFFFF); + renderPopupButton(g, PopupElement.TARGET, getPopupButtonText(PopupElement.TARGET), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.SIDE, getPopupButtonText(PopupElement.SIDE), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.X_MINUS, Component.literal("-"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.X_PLUS, Component.literal("+"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.Y_MINUS, Component.literal("-"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.Y_PLUS, Component.literal("+"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.MAX_MINUS, Component.literal("-"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.MAX_PLUS, Component.literal("+"), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.COMPACT, getPopupButtonText(PopupElement.COMPACT), mouseX, mouseY, false); + renderPopupButton(g, PopupElement.RESET, BagTabs.translation("dock.reset"), mouseX, mouseY, !(configPopup.editOverride() || DockConfigManager.hasOverride(configPopup.screenKey()))); + renderPopupButton(g, PopupElement.DONE, BagTabs.translation("dock.done"), mouseX, mouseY, false); + int centerX = x + (POPUP_WIDTH / 2); + int labelTop = y + 24; + g.drawCenteredString(Minecraft.getInstance().font, Component.literal("X: " + configPopup.settings().xOffset()), centerX, labelTop + 42, 0xFFFFFF); + g.drawCenteredString(Minecraft.getInstance().font, Component.literal("Y: " + configPopup.settings().yOffset()), centerX, labelTop + 64, 0xFFFFFF); + g.drawCenteredString(Minecraft.getInstance().font, Component.literal("Tabs: " + configPopup.settings().maxTabs()), centerX, labelTop + 86, 0xFFFFFF); + + PopupElement hovered = getPopupElementAt(mouseX, mouseY); + if (hovered == PopupElement.X_MINUS || hovered == PopupElement.X_PLUS || hovered == PopupElement.Y_MINUS || hovered == PopupElement.Y_PLUS) { + g.renderTooltip(Minecraft.getInstance().font, List.of(BagTabs.translation("dock.offset_steps").getVisualOrderText()), mouseX, mouseY); + } + g.pose().popPose(); + } + + private static void renderPopupButton(GuiGraphics g, PopupElement element, Component message, int mouseX, int mouseY, boolean disabled) { + Rect rect = getPopupRect(element); + int fill = disabled ? 0xFF3A3A3A : isPointInsideRect(mouseX, mouseY, rect) ? 0xFF7A7A7A : 0xFF5A5A5A; + int text = disabled ? 0xFF808080 : 0xFFFFFFFF; + g.fill(rect.x(), rect.y(), rect.right(), rect.bottom(), fill); + g.fill(rect.x(), rect.y(), rect.right(), rect.y() + 1, 0xFF000000); + g.fill(rect.x(), rect.bottom() - 1, rect.right(), rect.bottom(), 0xFF000000); + g.fill(rect.x(), rect.y(), rect.x() + 1, rect.bottom(), 0xFF000000); + g.fill(rect.right() - 1, rect.y(), rect.right(), rect.bottom(), 0xFF000000); + Minecraft minecraft = Minecraft.getInstance(); + int availableWidth = rect.width() - 6; + int textWidth = minecraft.font.width(message); + float scale = textWidth <= availableWidth ? 1.0F : Math.max(0.45F, availableWidth / (float) textWidth); + float scaledWidth = textWidth * scale; + float textX = rect.x() + ((rect.width() - scaledWidth) / 2.0F); + float textY = rect.y() + ((rect.height() - (minecraft.font.lineHeight * scale)) / 2.0F); + g.pose().pushPose(); + g.pose().translate(textX, textY, 0.0F); + g.pose().scale(scale, scale, 1.0F); + g.drawString(minecraft.font, message, 0, 0, text, false); + g.pose().popPose(); + } + + private static Component getPopupButtonText(PopupElement element) { + return switch (element) { + case TARGET -> configPopup.editOverride() + ? BagTabs.translation("dock.target.override") + : BagTabs.translation("dock.target.global"); + case SIDE -> BagTabs.translation("dock.side." + configPopup.settings().dockSide().name().toLowerCase()); + case COMPACT -> BagTabs.translation(configPopup.settings().compact() ? "dock.compact.on" : "dock.compact.off"); + default -> Component.empty(); + }; + } + + private static Rect getPopupRect(PopupElement element) { + int left = configPopup.x() + 6; + int top = configPopup.y() + 24; + return switch (element) { + case TARGET -> new Rect(left, top, 128, 14); + case SIDE -> new Rect(left, top + 18, 128, 14); + case X_MINUS -> new Rect(left, top + 40, 14, 14); + case X_PLUS -> new Rect(left + 114, top + 40, 14, 14); + case Y_MINUS -> new Rect(left, top + 62, 14, 14); + case Y_PLUS -> new Rect(left + 114, top + 62, 14, 14); + case MAX_MINUS -> new Rect(left, top + 84, 14, 14); + case MAX_PLUS -> new Rect(left + 114, top + 84, 14, 14); + case COMPACT -> new Rect(left, top + 100, 128, 14); + case RESET -> new Rect(left, top + 120, 61, 14); + case DONE -> new Rect(left + 67, top + 120, 61, 14); + }; + } + + private static PopupElement getPopupElementAt(double mouseX, double mouseY) { + if (configPopup == null) { + return null; + } + for (PopupElement element : PopupElement.values()) { + if (isPointInsideRect(mouseX, mouseY, getPopupRect(element))) { + return element; + } + } + return null; + } + + private static boolean isPointInsideConfigPopup(double mouseX, double mouseY) { + return configPopup != null && mouseX >= configPopup.x() && mouseX < configPopup.x() + POPUP_WIDTH && mouseY >= configPopup.y() && mouseY < configPopup.y() + POPUP_HEIGHT; + } + + private static boolean isPointInsideConfigPopupTitle(double mouseX, double mouseY) { + return configPopup != null + && mouseX >= configPopup.x() + 2 + && mouseX < configPopup.x() + POPUP_WIDTH - 2 + && mouseY >= configPopup.y() + 2 + && mouseY < configPopup.y() + POPUP_TITLE_HEIGHT; + } + + private static boolean isPointInsideRect(double mouseX, double mouseY, Rect rect) { + return mouseX >= rect.x() && mouseX < rect.right() && mouseY >= rect.y() && mouseY < rect.bottom(); + } + + private static int getOffsetStep() { + boolean shift = Screen.hasShiftDown(); + boolean control = Screen.hasControlDown(); + if (shift && control) { + return 100; + } + if (control) { + return 25; + } + if (shift) { + return 5; + } + return 1; + } + + private static void mutateConfigPopup(java.util.function.UnaryOperator mutator) { + if (configPopup == null) { + return; + } + configPopup = configPopup.withSettings(mutator.apply(configPopup.settings())); + saveConfigPopup(); + } + + private static void saveConfigPopup() { + if (configPopup != null) { + DockConfigManager.setEditableSettings(configPopup.screenKey(), configPopup.editOverride(), configPopup.settings()); + } + } + private static void rememberOpenedBag(BagEntry entry) { lastOpenedBagSlot = entry.slot(); lastOpenedBagKey = entry.identity() == null ? null : entry.identity().key(); @@ -1146,6 +1443,14 @@ public final class BagTabOverlay { return screen.getClass().getName(); } + private static int effectiveMouseX(int fallback) { + return overlayMouseX == Integer.MIN_VALUE ? fallback : overlayMouseX; + } + + private static int effectiveMouseY(int fallback) { + return overlayMouseY == Integer.MIN_VALUE ? fallback : overlayMouseY; + } + private static int clamp(int value, int min, int max) { return Math.max(min, Math.min(max, value)); } @@ -1257,6 +1562,34 @@ public final class BagTabOverlay { private record DockLayout(int firstTabX, int firstTabY, int tabWidth, int tabHeight, int controlX, int controlY, int controlWidth, int controlHeight, float scale, DockConfigManager.DockSide dockSide, boolean vertical, int maxTabs, boolean floating, boolean compact) { } + private record ConfigPopupState(String screenKey, boolean editOverride, DockConfigManager.DockSettings settings, int x, int y, boolean dragging, int dragOffsetX, int dragOffsetY) { + private ConfigPopupState withMode(boolean nextEditOverride, DockConfigManager.DockSettings nextSettings) { + return new ConfigPopupState(this.screenKey, nextEditOverride, nextSettings, this.x, this.y, false, 0, 0); + } + + private ConfigPopupState withSettings(DockConfigManager.DockSettings nextSettings) { + return new ConfigPopupState(this.screenKey, this.editOverride, nextSettings, this.x, this.y, this.dragging, this.dragOffsetX, this.dragOffsetY); + } + + private ConfigPopupState withPosition(int nextX, int nextY) { + return new ConfigPopupState(this.screenKey, this.editOverride, this.settings, nextX, nextY, this.dragging, this.dragOffsetX, this.dragOffsetY); + } + + private ConfigPopupState withDragging(boolean nextDragging, int nextDragOffsetX, int nextDragOffsetY) { + return new ConfigPopupState(this.screenKey, this.editOverride, this.settings, this.x, this.y, nextDragging, nextDragOffsetX, nextDragOffsetY); + } + } + + private record Rect(int x, int y, int width, int height) { + private int right() { + return this.x + this.width; + } + + private int bottom() { + return this.y + this.height; + } + } + private record PendingCursorRestore(double guiX, double guiY, long expiresAt) { } @@ -1354,4 +1687,18 @@ public final class BagTabOverlay { PREV, NEXT } + + private enum PopupElement { + TARGET, + SIDE, + X_MINUS, + X_PLUS, + Y_MINUS, + Y_PLUS, + MAX_MINUS, + MAX_PLUS, + COMPACT, + RESET, + DONE + } } diff --git a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java index 11e12a4..7fa85db 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java @@ -9,10 +9,18 @@ import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; public class DockConfigScreen extends Screen { + private static final int PANEL_WIDTH = 196; + private static final int PANEL_HEIGHT = 234; + private static final int TITLE_BAR_HEIGHT = 18; private final Screen parent; private final String screenKey; private boolean editOverride; private DockConfigManager.DockSettings dockSettings; + private int panelX; + private int panelY; + private boolean draggingPanel; + private int dragOffsetX; + private int dragOffsetY; private Button targetButton; private Button sideButton; @@ -24,6 +32,7 @@ public class DockConfigScreen extends Screen { private Button maxTabsPlusButton; private Button compactButton; private Button resetButton; + private Button doneButton; public DockConfigScreen(Screen parent) { super(BagTabs.translation("dock.title")); @@ -35,52 +44,54 @@ public class DockConfigScreen extends Screen { @Override protected void init() { - int centerX = this.width / 2; - int top = this.height / 2 - 70; + if (this.panelX == 0 && this.panelY == 0) { + this.panelX = (this.width - PANEL_WIDTH) / 2; + this.panelY = (this.height - PANEL_HEIGHT) / 2; + } this.targetButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> { saveCurrent(); this.editOverride = !this.editOverride; this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, this.editOverride).copy(); syncLabels(); - }).bounds(centerX - 90, top, 180, 20).build()); + }).bounds(0, 0, 180, 20).build()); this.sideButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> { this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().next()); saveCurrent(); syncLabels(); - }).bounds(centerX - 90, top + 26, 180, 20).build()); + }).bounds(0, 0, 180, 20).build()); this.xMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustX(-getOffsetStep())) - .bounds(centerX - 90, top + 56, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.xPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustX(getOffsetStep())) - .bounds(centerX + 70, top + 56, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.yMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustY(-getOffsetStep())) - .bounds(centerX - 90, top + 82, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.yPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustY(getOffsetStep())) - .bounds(centerX + 70, top + 82, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.maxTabsMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustMaxTabs(-1)) - .bounds(centerX - 90, top + 108, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.maxTabsPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustMaxTabs(1)) - .bounds(centerX + 70, top + 108, 20, 20).build()); + .bounds(0, 0, 20, 20).build()); this.compactButton = this.addRenderableWidget(Button.builder(Component.empty(), button -> { this.dockSettings = this.dockSettings.withCompact(!this.dockSettings.compact()); saveCurrent(); syncLabels(); - }).bounds(centerX - 90, top + 134, 180, 20).build()); + }).bounds(0, 0, 180, 20).build()); this.resetButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.reset"), button -> { DockConfigManager.clearOverride(this.screenKey); this.editOverride = false; this.dockSettings = DockConfigManager.getEditableSettings(this.screenKey, false).copy(); syncLabels(); - }).bounds(centerX - 90, top + 160, 180, 20).build()); + }).bounds(0, 0, 180, 20).build()); - this.addRenderableWidget(Button.builder(BagTabs.translation("dock.done"), button -> onClose()) - .bounds(centerX - 90, top + 186, 180, 20).build()); + this.doneButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.done"), button -> onClose()) + .bounds(0, 0, 180, 20).build()); syncLabels(); Tooltip offsetTooltip = Tooltip.create(BagTabs.translation("dock.offset_steps")); @@ -88,16 +99,22 @@ public class DockConfigScreen extends Screen { this.xPlusButton.setTooltip(offsetTooltip); this.yMinusButton.setTooltip(offsetTooltip); this.yPlusButton.setTooltip(offsetTooltip); + updateWidgetPositions(); } @Override public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { - this.renderBackground(guiGraphics, mouseX, mouseY, partialTick); + this.parent.render(guiGraphics, mouseX, mouseY, partialTick); + guiGraphics.fill(0, 0, this.width, this.height, 0x5A000000); + guiGraphics.fill(this.panelX, this.panelY, this.panelX + PANEL_WIDTH, this.panelY + PANEL_HEIGHT, 0xE03A3A3A); + guiGraphics.fill(this.panelX + 1, this.panelY + 1, this.panelX + PANEL_WIDTH - 1, this.panelY + PANEL_HEIGHT - 1, 0xE01E1E1E); + guiGraphics.fill(this.panelX + 2, this.panelY + 2, this.panelX + PANEL_WIDTH - 2, this.panelY + TITLE_BAR_HEIGHT, 0xFF5A5A5A); + guiGraphics.fill(this.panelX + 2, this.panelY + TITLE_BAR_HEIGHT, this.panelX + PANEL_WIDTH - 2, this.panelY + TITLE_BAR_HEIGHT + 1, 0xFF000000); super.render(guiGraphics, mouseX, mouseY, partialTick); - int centerX = this.width / 2; - int top = this.height / 2 - 70; - guiGraphics.drawCenteredString(this.font, this.title, centerX, top - 18, 0xFFFFFF); + int centerX = this.panelX + (PANEL_WIDTH / 2); + int top = this.panelY + 12; + guiGraphics.drawCenteredString(this.font, this.title, centerX, this.panelY + 6, 0xFFFFFF); guiGraphics.drawCenteredString(this.font, Component.literal("X Offset: " + this.dockSettings.xOffset()), centerX, top + 62, 0xFFFFFF); guiGraphics.drawCenteredString(this.font, Component.literal("Y Offset: " + this.dockSettings.yOffset()), centerX, top + 88, 0xFFFFFF); guiGraphics.drawCenteredString(this.font, Component.literal("Max Tabs: " + this.dockSettings.maxTabs()), centerX, top + 114, 0xFFFFFF); @@ -109,8 +126,19 @@ public class DockConfigScreen extends Screen { Minecraft.getInstance().setScreen(this.parent); } + @Override + public boolean isPauseScreen() { + return false; + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && isOverTitleBar(mouseX, mouseY)) { + this.draggingPanel = true; + this.dragOffsetX = (int) mouseX - this.panelX; + this.dragOffsetY = (int) mouseY - this.panelY; + return true; + } if (button == 1 && this.sideButton != null && this.sideButton.isMouseOver(mouseX, mouseY)) { this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().previous()); saveCurrent(); @@ -120,6 +148,25 @@ public class DockConfigScreen extends Screen { return super.mouseClicked(mouseX, mouseY, button); } + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (button == 0 && this.draggingPanel) { + this.panelX = clamp((int) mouseX - this.dragOffsetX, 4, this.width - PANEL_WIDTH - 4); + this.panelY = clamp((int) mouseY - this.dragOffsetY, 4, this.height - PANEL_HEIGHT - 4); + updateWidgetPositions(); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0) { + this.draggingPanel = false; + } + return super.mouseReleased(mouseX, mouseY, button); + } + private void adjustX(int delta) { this.dockSettings = this.dockSettings.withXOffset(this.dockSettings.xOffset() + delta); saveCurrent(); @@ -165,4 +212,31 @@ public class DockConfigScreen extends Screen { private void saveCurrent() { DockConfigManager.setEditableSettings(this.screenKey, this.editOverride, this.dockSettings); } + + private void updateWidgetPositions() { + int left = this.panelX + 8; + int top = this.panelY + 26; + this.targetButton.setPosition(left, top); + this.sideButton.setPosition(left, top + 26); + this.xMinusButton.setPosition(left, top + 56); + this.xPlusButton.setPosition(left + 160, top + 56); + this.yMinusButton.setPosition(left, top + 82); + this.yPlusButton.setPosition(left + 160, top + 82); + this.maxTabsMinusButton.setPosition(left, top + 108); + this.maxTabsPlusButton.setPosition(left + 160, top + 108); + this.compactButton.setPosition(left, top + 134); + this.resetButton.setPosition(left, top + 160); + this.doneButton.setPosition(left, top + 186); + } + + private boolean isOverTitleBar(double mouseX, double mouseY) { + return mouseX >= this.panelX + 2 + && mouseX < this.panelX + PANEL_WIDTH - 2 + && mouseY >= this.panelY + 2 + && mouseY < this.panelY + TITLE_BAR_HEIGHT; + } + + private int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } } diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_config_popup.png b/src/main/resources/assets/bagtabs/textures/gui/dock_config_popup.png new file mode 100644 index 0000000..c2ff23c Binary files /dev/null and b/src/main/resources/assets/bagtabs/textures/gui/dock_config_popup.png differ