From 638fce4116b89212feaba7c65334ad1fdd282298 Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Sun, 22 Mar 2026 23:46:35 -0400 Subject: [PATCH] tidying up UI and UX. Add screen dock modes (left/right/top/bottom), better X/Y offset control, replaced tab size with "max tabs" --- .../com/trunksbomb/bagtabs/BagTabsClient.java | 5 + .../bagtabs/client/BagTabOverlay.java | 788 +++++++++++------- .../bagtabs/client/DockConfigManager.java | 46 +- .../bagtabs/client/DockConfigScreen.java | 54 +- .../resources/assets/bagtabs/lang/en_us.json | 10 +- 5 files changed, 580 insertions(+), 323 deletions(-) diff --git a/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java b/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java index bc3a4d5..5d3d04e 100644 --- a/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java +++ b/src/main/java/com/trunksbomb/bagtabs/BagTabsClient.java @@ -23,6 +23,7 @@ public class BagTabsClient { NeoForge.EVENT_BUS.addListener(BagTabsClient::clickTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::dragTabs); NeoForge.EVENT_BUS.addListener(BagTabsClient::releaseTabs); + NeoForge.EVENT_BUS.addListener(BagTabsClient::initScreens); } private static void registerScreens(RegisterMenuScreensEvent event) { @@ -51,6 +52,10 @@ public class BagTabsClient { BagTabOverlay.mouseDragged(event); } + private static void initScreens(ScreenEvent.Init.Post event) { + BagTabOverlay.screenInit(event); + } + private static final class BagItemColor { private static final int DEFAULT_TINT = 0x9E7B4F; diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index bdd8f01..03dc04c 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -1,35 +1,41 @@ package com.trunksbomb.bagtabs.client; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.math.Axis; import com.trunksbomb.bagtabs.BagTabs; import com.trunksbomb.bagtabs.bag.BagAccess; import com.trunksbomb.bagtabs.bag.BagCompat; import com.trunksbomb.bagtabs.bag.BagEntry; import com.trunksbomb.bagtabs.item.BagItem; -import com.trunksbomb.bagtabs.network.InsertIntoBagPayload; -import com.trunksbomb.bagtabs.network.QueryInsertTargetsPayload; import com.trunksbomb.bagtabs.menu.BagMenu; +import com.trunksbomb.bagtabs.network.InsertIntoBagPayload; import com.trunksbomb.bagtabs.network.OpenBagPayload; -import com.mojang.blaze3d.systems.RenderSystem; +import com.trunksbomb.bagtabs.network.QueryInsertTargetsPayload; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import net.minecraft.Util; +import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.core.component.DataComponents; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner; +import net.minecraft.client.gui.screens.inventory.tooltip.MenuTooltipPositioner; import net.minecraft.client.resources.sounds.SimpleSoundInstance; -import net.minecraft.ChatFormatting; +import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.component.DyedItemColor; -import net.minecraft.world.inventory.Slot; import net.neoforged.neoforge.client.event.ScreenEvent; import net.neoforged.neoforge.network.PacketDistributor; +import org.lwjgl.glfw.GLFW; public final class BagTabOverlay { private static final ResourceLocation TAB_BASE_TEXTURE = BagTabs.id("textures/gui/bag_tabs_base.png"); @@ -49,19 +55,20 @@ public final class BagTabOverlay { private static final Set INSERTABLE_SLOTS = new HashSet<>(); private static ItemStack lastCarriedStack = ItemStack.EMPTY; private static int lastMenuContainerId = Integer.MIN_VALUE; - private static int lastInsertRequestId = 0; + private static int lastInsertRequestId; private static int pendingInsertRequestId = -1; private static PendingClick pendingClick; private static DragState dragState; + private static PendingCursorRestore pendingCursorRestore; private BagTabOverlay() { } public static void render(ScreenEvent.Render.Post event) { + expirePendingCursorRestore(); if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen)) { return; } - Player player = Minecraft.getInstance().player; if (player == null) { return; @@ -72,66 +79,63 @@ public final class BagTabOverlay { return; } - GuiGraphics guiGraphics = event.getGuiGraphics(); + GuiGraphics g = event.getGuiGraphics(); int mouseX = event.getMouseX(); int mouseY = event.getMouseY(); - ItemStack carriedStack = screen.getMenu().getCarried(); + ItemStack carried = screen.getMenu().getCarried(); DockLayout layout = getDockLayout(screen, player, tabs.size()); DockControl control = getDockControl(layout); + refreshInsertTargets(screen, carried); - refreshInsertTargets(screen, carriedStack); - - if (control != null) { - renderDockControl(guiGraphics, control, mouseX, mouseY); - } - - RenderedTab draggedTab = null; + RenderedTab dragged = null; for (RenderedTab tab : tabs) { if (dragState != null && tab.entry().slot() == dragState.draggedSlot()) { - draggedTab = tab; + dragged = tab; continue; } - renderTab(guiGraphics, tab, mouseX, mouseY, carriedStack, false); - guiGraphics.renderItem(tab.entry().stack(), tab.itemX(), tab.itemY()); + renderTab(g, tab, mouseX, mouseY, carried, false); + g.renderItem(tab.entry().stack(), tab.itemX(), tab.itemY()); + renderPinOverlay(g, tab); } - if (draggedTab != null) { - int dragX = (int) Math.round(mouseX - dragState.grabOffsetX()); - RenderedTab floatingTab = draggedTab.withPosition(dragX, draggedTab.y()); - guiGraphics.pose().pushPose(); - guiGraphics.pose().translate(0.0F, 0.0F, 200.0F); - renderTab(guiGraphics, floatingTab, mouseX, mouseY, carriedStack, true); - guiGraphics.renderItem(draggedTab.entry().stack(), floatingTab.itemX(), floatingTab.itemY()); - guiGraphics.pose().popPose(); + if (layout.dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL + || layout.dockSide() == DockConfigManager.DockSide.FLOATING_VERTICAL) { + g.pose().pushPose(); + g.pose().translate(0.0F, 0.0F, 150.0F); + renderDockControl(g, control, mouseX, mouseY); + g.pose().popPose(); + } else { + renderDockControl(g, control, mouseX, mouseY); } - if (dragState == null) { - if (control != null) { - DockIcon hoveredIcon = control.hoveredIcon(mouseX, mouseY); - if (hoveredIcon != null) { - guiGraphics.renderTooltip( - Minecraft.getInstance().font, - List.of(getDockTooltip(hoveredIcon).getVisualOrderText()), - mouseX, - mouseY - ); - return; - } - } + if (dragged != null) { + int dragX = dragged.isVertical() ? dragged.x() : (int) Math.round(mouseX - dragState.grabOffsetX()); + int dragY = dragged.isVertical() ? (int) Math.round(mouseY - dragState.grabOffsetY()) : dragged.y(); + RenderedTab floating = dragged.withPosition(dragX, dragY); + g.pose().pushPose(); + g.pose().translate(0.0F, 0.0F, 200.0F); + renderTab(g, floating, mouseX, mouseY, carried, true); + g.renderItem(dragged.entry().stack(), floating.itemX(), floating.itemY()); + renderPinOverlay(g, floating); + g.pose().popPose(); + } - for (RenderedTab tab : tabs) { - if (!tab.isHovered(mouseX, mouseY)) { - continue; - } + if (dragState != null) { + return; + } - guiGraphics.renderTooltip( - Minecraft.getInstance().font, - getTooltipLines(tab, tabs).stream().map(Component::getVisualOrderText).toList(), - mouseX, - mouseY - ); - break; + DockIcon hovered = control.hoveredIcon(mouseX, mouseY); + if (hovered != null) { + g.renderTooltip(Minecraft.getInstance().font, List.of(getDockTooltip(hovered).getVisualOrderText()), createTooltipPositioner(screen), mouseX, mouseY); + return; + } + + for (RenderedTab tab : tabs) { + if (!tab.isHovered(mouseX, mouseY)) { + continue; } + g.renderTooltip(Minecraft.getInstance().font, getTooltipLines(tab, tabs).stream().map(Component::getVisualOrderText).toList(), createTooltipPositioner(screen), mouseX, mouseY); + break; } } @@ -139,7 +143,6 @@ public final class BagTabOverlay { if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen)) { return; } - Player player = Minecraft.getInstance().player; if (player == null) { return; @@ -147,24 +150,21 @@ public final class BagTabOverlay { List tabs = getRenderedTabs(screen, player); DockControl control = getDockControl(getDockLayout(screen, player, tabs.size())); - if (event.getButton() == 0) { - if (control != null) { - DockIcon clickedIcon = control.hoveredIcon(event.getMouseX(), event.getMouseY()); - if (clickedIcon == DockIcon.LOCK) { - DockConfigManager.toggleInteractionsLocked(); - dragState = null; - pendingClick = null; - Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - event.setCanceled(true); - return; - } - if (clickedIcon == DockIcon.CONFIG) { - Minecraft.getInstance().setScreen(new DockConfigScreen(screen)); - Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - event.setCanceled(true); - return; - } + DockIcon clicked = control.hoveredIcon(event.getMouseX(), event.getMouseY()); + if (clicked == DockIcon.LOCK) { + DockConfigManager.toggleInteractionsLocked(); + dragState = null; + pendingClick = null; + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + event.setCanceled(true); + return; + } + if (clicked == DockIcon.CONFIG) { + Minecraft.getInstance().setScreen(new DockConfigScreen(screen)); + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + event.setCanceled(true); + return; } } @@ -176,7 +176,6 @@ public final class BagTabOverlay { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; } - TabPinManager.ToggleResult result = TabPinManager.togglePin(tab.entry(), tabs.stream().map(RenderedTab::entry).toList()); if (result.changed()) { Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); @@ -192,13 +191,13 @@ public final class BagTabOverlay { if (event.getButton() != 0 || !screen.getMenu().getCarried().isEmpty()) { return; } - for (RenderedTab tab : tabs) { - if (tab.isHovered(event.getMouseX(), event.getMouseY())) { - pendingClick = new PendingClick(tab.entry().slot(), event.getMouseX() - tab.x(), event.getMouseY() - tab.y(), tab.pinned() && !DockConfigManager.isInteractionsLocked()); - event.setCanceled(true); - return; + if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { + continue; } + pendingClick = new PendingClick(tab.entry().slot(), event.getMouseX() - tab.x(), event.getMouseY() - tab.y(), tab.pinned() && !DockConfigManager.isInteractionsLocked()); + event.setCanceled(true); + return; } } @@ -206,7 +205,6 @@ public final class BagTabOverlay { if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen) || event.getMouseButton() != 0) { return; } - Player player = Minecraft.getInstance().player; if (player == null) { return; @@ -214,28 +212,19 @@ public final class BagTabOverlay { List tabs = getRenderedTabs(screen, player); if (pendingClick != null && pendingClick.pinned()) { - RenderedTab draggedTab = tabs.stream() - .filter(tab -> tab.entry().slot() == pendingClick.slot()) - .findFirst() - .orElse(null); - if (draggedTab != null) { - double dragDistance = Math.hypot(event.getMouseX() - (draggedTab.x() + pendingClick.grabOffsetX()), event.getMouseY() - (draggedTab.y() + pendingClick.grabOffsetY())); - if (dragDistance > 3.0D) { - dragState = new DragState( - pendingClick.slot(), - pendingClick.grabOffsetX(), - TabPinManager.getPinnedIdentityOrder(tabs.stream().map(RenderedTab::entry).toList()) - ); + RenderedTab dragged = tabs.stream().filter(tab -> tab.entry().slot() == pendingClick.slot()).findFirst().orElse(null); + if (dragged != null) { + double distance = Math.hypot(event.getMouseX() - (dragged.x() + pendingClick.grabOffsetX()), event.getMouseY() - (dragged.y() + pendingClick.grabOffsetY())); + if (distance > 3.0D) { + dragState = new DragState(pendingClick.slot(), pendingClick.grabOffsetX(), pendingClick.grabOffsetY(), TabPinManager.getPinnedIdentityOrder(tabs.stream().map(RenderedTab::entry).toList())); pendingClick = null; } } } - if (dragState == null) { return; } - - updateDragPreview(event.getMouseX(), tabs); + updateDragPreview(event.getMouseX(), event.getMouseY(), tabs); event.setCanceled(true); } @@ -243,14 +232,13 @@ public final class BagTabOverlay { if (!(event.getScreen() instanceof AbstractContainerScreen screen) || !supportsTabs(screen) || event.getButton() != 0) { return; } - Player player = Minecraft.getInstance().player; if (player == null) { return; } - ItemStack carriedStack = screen.getMenu().getCarried(); - if (carriedStack.isEmpty()) { + ItemStack carried = screen.getMenu().getCarried(); + if (carried.isEmpty()) { List tabs = getRenderedTabs(screen, player); if (dragState != null) { TabPinManager.applyPinnedOrder(dragState.previewOrder(), tabs.stream().map(RenderedTab::entry).toList()); @@ -260,10 +248,10 @@ public final class BagTabOverlay { event.setCanceled(true); return; } - if (pendingClick != null) { for (RenderedTab tab : tabs) { if (tab.entry().slot() == pendingClick.slot() && tab.isHovered(event.getMouseX(), event.getMouseY())) { + scheduleCursorRestore(event.getMouseX(), event.getMouseY()); PacketDistributor.sendToServer(new OpenBagPayload(tab.entry().slot())); Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); event.setCanceled(true); @@ -279,12 +267,10 @@ public final class BagTabOverlay { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; } - if (!INSERTABLE_SLOTS.contains(tab.entry().slot())) { event.setCanceled(true); return; } - PacketDistributor.sendToServer(new InsertIntoBagPayload(tab.entry().slot())); Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.ITEM_PICKUP, 0.8F)); event.setCanceled(true); @@ -292,11 +278,30 @@ public final class BagTabOverlay { } } + public static void screenInit(ScreenEvent.Init.Post event) { + if (pendingCursorRestore == null) { + return; + } + expirePendingCursorRestore(); + if (pendingCursorRestore == null || !(event.getScreen() instanceof AbstractContainerScreen)) { + return; + } + + Minecraft minecraft = Minecraft.getInstance(); + double guiScale = minecraft.getWindow().getGuiScale(); + GLFW.glfwSetCursorPos( + minecraft.getWindow().getWindow(), + pendingCursorRestore.guiX() * guiScale, + pendingCursorRestore.guiY() * guiScale + ); + minecraft.mouseHandler.setIgnoreFirstMove(); + pendingCursorRestore = null; + } + public static void updateInsertTargets(int requestId, List insertableSlots) { if (requestId != pendingInsertRequestId) { return; } - INSERTABLE_SLOTS.clear(); INSERTABLE_SLOTS.addAll(insertableSlots); pendingInsertRequestId = -1; @@ -307,79 +312,63 @@ public final class BagTabOverlay { if (dragState != null) { bags = applyPreviewOrder(bags, dragState.previewOrder()); } - List renderedTabs = new ArrayList<>(); - int activeBagSlot = getActiveBagSlot(screen); DockLayout layout = getDockLayout(screen, player, bags.size()); + List rendered = new ArrayList<>(); + int activeBagSlot = getActiveBagSlot(screen); int x = layout.firstTabX(); - int y = layout.tabY(); - int maxX = getMaxTabX(screen, player, layout); + int y = layout.firstTabY(); + int maxPrimary = getMaxPrimary(screen, player, layout); + int renderedCount = 0; for (BagEntry bag : bags) { - if (x > maxX) { + if (renderedCount >= layout.maxTabs()) { break; } - - boolean pinned = TabPinManager.isPinned(bag, bags); - renderedTabs.add(new RenderedTab( - bag, - x, - y, - bag.slot() == activeBagSlot, - pinned, - x + Math.max(2, Math.round(layout.scale() * 3.0F)), - y + Math.max(1, Math.round(layout.scale() * 2.0F)), - layout.tabWidth(), - layout.tabHeight() - )); - x += layout.tabWidth() + TAB_GAP; + if (!layout.floating() && (layout.vertical() ? y : x) > maxPrimary) { + break; + } + rendered.add(new RenderedTab(bag, x, y, bag.slot() == activeBagSlot, TabPinManager.isPinned(bag, bags), layout.dockSide(), layout.scale(), layout.tabWidth(), layout.tabHeight())); + if (layout.vertical()) { + y += layout.tabHeight() + TAB_GAP; + } else { + x += layout.tabWidth() + TAB_GAP; + } + renderedCount++; } - - return renderedTabs; + return rendered; } private static List applyPreviewOrder(List bags, List previewOrder) { List pinned = bags.stream().filter(bag -> TabPinManager.isPinned(bag, bags)).toList(); List unpinned = bags.stream().filter(bag -> !TabPinManager.isPinned(bag, bags)).toList(); List orderedPinned = new ArrayList<>(); - for (String identityKey : previewOrder) { - pinned.stream() - .filter(bag -> bag.identity() != null && identityKey.equals(bag.identity().key())) - .findFirst() - .ifPresent(orderedPinned::add); + for (String key : previewOrder) { + pinned.stream().filter(bag -> bag.identity() != null && key.equals(bag.identity().key())).findFirst().ifPresent(orderedPinned::add); } for (BagEntry bag : pinned) { if (!orderedPinned.contains(bag)) { orderedPinned.add(bag); } } - List ordered = new ArrayList<>(orderedPinned); ordered.addAll(unpinned); return ordered; } private static int getInventoryLeftBound(AbstractContainerScreen screen, Player player) { - return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream() - .mapToInt(slot -> slot.x) - .min() - .orElse(8); + return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream().mapToInt(slot -> slot.x).min().orElse(8); } private static int getInventoryRightBound(AbstractContainerScreen screen, Player player) { - return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream() - .mapToInt(slot -> slot.x + 16) - .max() - .orElse(screen.getXSize() - 8); + return screen.getGuiLeft() + getPlayerInventorySlots(screen, player).stream().mapToInt(slot -> slot.x + 16).max().orElse(screen.getXSize() - 8); } private static List getPlayerInventorySlots(AbstractContainerScreen screen, Player player) { - List slots = screen.getMenu().slots.stream() - .filter(slot -> slot.container == player.getInventory()) - .toList(); + List slots = screen.getMenu().slots.stream().filter(slot -> slot.container == player.getInventory()).toList(); return slots.isEmpty() ? screen.getMenu().slots : slots; } - private static void renderTab(GuiGraphics guiGraphics, RenderedTab tab, int mouseX, int mouseY, ItemStack carriedStack, boolean dragged) { + private static void renderTab(GuiGraphics g, RenderedTab tab, int mouseX, int mouseY, ItemStack carried, boolean dragged) { boolean hovered = tab.isHovered(mouseX, mouseY); boolean selected = tab.selected(); float red = 1.0F; @@ -394,32 +383,27 @@ public final class BagTabOverlay { int uOffset = (hovered || selected) ? BASE_TAB_WIDTH : 0; RenderSystem.setShaderColor(red, green, blue, 1.0F); - blitScaled(guiGraphics, TAB_BASE_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT); + blitRotated(g, TAB_BASE_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT, tab.rotationDegrees()); RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - blitScaled(guiGraphics, TAB_OVERLAY_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT); + blitRotated(g, TAB_OVERLAY_TEXTURE, tab.x(), tab.y(), tab.width(), tab.height(), uOffset, 0, BASE_TAB_WIDTH, BASE_TAB_HEIGHT, BASE_TAB_WIDTH * 2, BASE_TAB_HEIGHT, tab.rotationDegrees()); - if (dragged) { - guiGraphics.fill(tab.x() + 1, tab.y(), tab.x() + tab.width() - 1, tab.y() + 1, 0xFF000000); - guiGraphics.fill(tab.x() + 2, tab.y() + 1, tab.x() + tab.width() - 2, tab.y() + 2, 0x80FFFFFF); + g.pose().pushPose(); + pushTabTransform(g, tab); + if (dragged || tab.usesFloatingArt()) { + g.fill(1, 0, BASE_TAB_WIDTH - 1, 1, 0xFF000000); + g.fill(2, 1, BASE_TAB_WIDTH - 2, 2, 0x80FFFFFF); } - if (selected) { - guiGraphics.fill(tab.x() + 3, tab.y() + tab.height() - 4, tab.x() + tab.width() - 3, tab.y() + tab.height() - 2, 0xFFD94A4A); + g.fill(3, BASE_TAB_HEIGHT - 4, BASE_TAB_WIDTH - 3, BASE_TAB_HEIGHT - 2, 0xFFD94A4A); } - - if (tab.pinned()) { - guiGraphics.fill(tab.x() + 2, tab.y() + 4, tab.x() + 6, tab.y() + 5, 0xFFFFDA6B); - guiGraphics.fill(tab.x() + 3, tab.y() + 5, tab.x() + 5, tab.y() + 8, 0xFFFFDA6B); - guiGraphics.fill(tab.x() + 4, tab.y() + 8, tab.x() + 5, tab.y() + 9, 0xFFD94A4A); - } - - if (!carriedStack.isEmpty()) { + if (!carried.isEmpty()) { if (INSERTABLE_SLOTS.contains(tab.entry().slot())) { - renderPlusIndicator(guiGraphics, tab.x() + tab.width() - 7, tab.y() + 3); + renderPlusIndicator(g, BASE_TAB_WIDTH - 7, 3); } else { - renderXIndicator(guiGraphics, tab.x() + tab.width() - 7, tab.y() + 3); + renderXIndicator(g, BASE_TAB_WIDTH - 7, 3); } } + g.pose().popPose(); } private static boolean supportsTabs(AbstractContainerScreen screen) { @@ -430,45 +414,64 @@ public final class BagTabOverlay { return BagCompat.getActiveBagSlot(screen.getMenu()); } - private static void refreshInsertTargets(AbstractContainerScreen screen, ItemStack carriedStack) { - if (carriedStack.isEmpty()) { + private static void refreshInsertTargets(AbstractContainerScreen screen, ItemStack carried) { + if (carried.isEmpty()) { INSERTABLE_SLOTS.clear(); lastCarriedStack = ItemStack.EMPTY; lastMenuContainerId = screen.getMenu().containerId; pendingInsertRequestId = -1; return; } - boolean menuChanged = screen.getMenu().containerId != lastMenuContainerId; - boolean carriedChanged = !ItemStack.matches(lastCarriedStack, carriedStack); + boolean carriedChanged = !ItemStack.matches(lastCarriedStack, carried); if (!menuChanged && !carriedChanged) { return; } - - lastCarriedStack = carriedStack.copy(); + lastCarriedStack = carried.copy(); lastMenuContainerId = screen.getMenu().containerId; INSERTABLE_SLOTS.clear(); pendingInsertRequestId = ++lastInsertRequestId; PacketDistributor.sendToServer(new QueryInsertTargetsPayload(pendingInsertRequestId)); } - private static void renderPlusIndicator(GuiGraphics guiGraphics, int x, int y) { - guiGraphics.fill(x + 1, y, x + 4, y + 5, 0xFF000000); - guiGraphics.fill(x, y + 1, x + 5, y + 4, 0xFF000000); - guiGraphics.fill(x + 2, y + 1, x + 3, y + 4, 0xFF6CFF6C); - guiGraphics.fill(x + 1, y + 2, x + 4, y + 3, 0xFF6CFF6C); + private static void renderPlusIndicator(GuiGraphics g, int x, int y) { + g.fill(x + 1, y, x + 4, y + 5, 0xFF000000); + g.fill(x, y + 1, x + 5, y + 4, 0xFF000000); + g.fill(x + 2, y + 1, x + 3, y + 4, 0xFF6CFF6C); + g.fill(x + 1, y + 2, x + 4, y + 3, 0xFF6CFF6C); } - private static void renderXIndicator(GuiGraphics guiGraphics, int x, int y) { - guiGraphics.fill(x, y, x + 1, y + 1, 0xFFFF6C6C); - guiGraphics.fill(x + 4, y, x + 5, y + 1, 0xFFFF6C6C); - guiGraphics.fill(x + 1, y + 1, x + 2, y + 2, 0xFFFF6C6C); - guiGraphics.fill(x + 3, y + 1, x + 4, y + 2, 0xFFFF6C6C); - guiGraphics.fill(x + 2, y + 2, x + 3, y + 3, 0xFFFF6C6C); - guiGraphics.fill(x + 1, y + 3, x + 2, y + 4, 0xFFFF6C6C); - guiGraphics.fill(x + 3, y + 3, x + 4, y + 4, 0xFFFF6C6C); - guiGraphics.fill(x, y + 4, x + 1, y + 5, 0xFFFF6C6C); - guiGraphics.fill(x + 4, y + 4, x + 5, y + 5, 0xFFFF6C6C); + private static void renderXIndicator(GuiGraphics g, int x, int y) { + g.fill(x, y, x + 1, y + 1, 0xFFFF6C6C); + g.fill(x + 4, y, x + 5, y + 1, 0xFFFF6C6C); + g.fill(x + 1, y + 1, x + 2, y + 2, 0xFFFF6C6C); + g.fill(x + 3, y + 1, x + 4, y + 2, 0xFFFF6C6C); + g.fill(x + 2, y + 2, x + 3, y + 3, 0xFFFF6C6C); + g.fill(x + 1, y + 3, x + 2, y + 4, 0xFFFF6C6C); + g.fill(x + 3, y + 3, x + 4, y + 4, 0xFFFF6C6C); + g.fill(x, y + 4, x + 1, y + 5, 0xFFFF6C6C); + g.fill(x + 4, y + 4, x + 5, y + 5, 0xFFFF6C6C); + } + + private static void scheduleCursorRestore(double guiX, double guiY) { + pendingCursorRestore = new PendingCursorRestore(guiX, guiY, Util.getMillis() + 1000L); + } + + private static void expirePendingCursorRestore() { + if (pendingCursorRestore != null && Util.getMillis() > pendingCursorRestore.expiresAt()) { + pendingCursorRestore = null; + } + } + + private static void renderPinOverlay(GuiGraphics g, RenderedTab tab) { + if (!tab.pinned()) { + return; + } + int x = tab.x() + 2; + int y = tab.y() + 2; + g.fill(x, y, x + 4, y + 1, 0xFFFFDA6B); + g.fill(x + 1, y + 1, x + 3, y + 4, 0xFFFFDA6B); + g.fill(x + 2, y + 4, x + 3, y + 5, 0xFFD94A4A); } private static List getTooltipLines(RenderedTab tab, List tabs) { @@ -485,7 +488,6 @@ public final class BagTabOverlay { if (DockConfigManager.isInteractionsLocked()) { return lines; } - if (failureReason == null) { lines.add(Component.literal("Right-click: pin")); } else { @@ -495,153 +497,247 @@ public final class BagTabOverlay { return lines; } - private static void updateDragPreview(double mouseX, List tabs) { + private static void updateDragPreview(double mouseX, double mouseY, List tabs) { if (dragState == null) { return; } - List pinnedTabs = tabs.stream().filter(RenderedTab::pinned).toList(); - RenderedTab draggedTab = pinnedTabs.stream() - .filter(tab -> tab.entry().slot() == dragState.draggedSlot()) - .findFirst() - .orElse(null); - if (draggedTab == null) { + RenderedTab dragged = pinnedTabs.stream().filter(tab -> tab.entry().slot() == dragState.draggedSlot()).findFirst().orElse(null); + if (dragged == null) { return; } List reordered = new ArrayList<>(dragState.previewOrder()); - String draggedIdentity = draggedTab.entry().identity().key(); + String draggedIdentity = dragged.entry().identity().key(); int draggedIndex = reordered.indexOf(draggedIdentity); if (draggedIndex < 0) { return; } - double draggedCenter = mouseX; + double draggedPrimary = dragged.isVertical() ? mouseY : mouseX; boolean changed = false; - while (draggedIndex > 0) { String leftIdentity = reordered.get(draggedIndex - 1); - RenderedTab leftTab = pinnedTabs.stream() - .filter(tab -> tab.entry().identity() != null && leftIdentity.equals(tab.entry().identity().key())) - .findFirst() - .orElse(null); - if (leftTab == null || draggedCenter >= leftTab.centerX()) { + RenderedTab leftTab = pinnedTabs.stream().filter(tab -> tab.entry().identity() != null && leftIdentity.equals(tab.entry().identity().key())).findFirst().orElse(null); + if (leftTab == null || draggedPrimary >= leftTab.primaryCenter()) { break; } - reordered.set(draggedIndex, leftIdentity); reordered.set(draggedIndex - 1, draggedIdentity); draggedIndex--; changed = true; } - while (draggedIndex < reordered.size() - 1) { String rightIdentity = reordered.get(draggedIndex + 1); - RenderedTab rightTab = pinnedTabs.stream() - .filter(tab -> tab.entry().identity() != null && rightIdentity.equals(tab.entry().identity().key())) - .findFirst() - .orElse(null); - if (rightTab == null || draggedCenter <= rightTab.centerX()) { + RenderedTab rightTab = pinnedTabs.stream().filter(tab -> tab.entry().identity() != null && rightIdentity.equals(tab.entry().identity().key())).findFirst().orElse(null); + if (rightTab == null || draggedPrimary <= rightTab.primaryCenter()) { break; } - reordered.set(draggedIndex, rightIdentity); reordered.set(draggedIndex + 1, draggedIdentity); draggedIndex++; changed = true; } - if (changed) { - dragState = new DragState(dragState.draggedSlot(), dragState.grabOffsetX(), reordered); + dragState = new DragState(dragState.draggedSlot(), dragState.grabOffsetX(), dragState.grabOffsetY(), reordered); } } private static DockLayout getDockLayout(AbstractContainerScreen screen, Player player, int bagCount) { - DockConfigManager.DockSettings dockSettings = DockConfigManager.getEffectiveSettings(getScreenKey(screen)); - float scale = dockSettings.scalePercent() / 100.0F; - int tabWidth = Math.max(16, Math.round(BASE_TAB_WIDTH * scale)); - int tabHeight = Math.max(16, Math.round(BASE_TAB_HEIGHT * scale)); - int controlWidth = Math.max(14, Math.round(BASE_DOCK_WIDTH * scale)); - int controlHeight = Math.max(16, Math.round(BASE_DOCK_HEIGHT * scale)); - int leftBound = getInventoryLeftBound(screen, player); - int rightBound = getInventoryRightBound(screen, player); - int topBound = getInventoryTopBound(screen, player); + DockConfigManager.DockSettings settings = DockConfigManager.getEffectiveSettings(getScreenKey(screen)); + float scale = 1.0F; + int tabWidth = BASE_TAB_WIDTH; + int tabHeight = BASE_TAB_HEIGHT; + int baseControlWidth = BASE_DOCK_WIDTH; + int baseControlHeight = BASE_DOCK_HEIGHT; + int inventoryLeftBound = getInventoryLeftBound(screen, player); + int inventoryRightBound = getInventoryRightBound(screen, player); int bottomBound = getInventoryBottomBound(screen, player); - int controlStripWidth = controlWidth; + int guiLeftBound = screen.getGuiLeft(); + int guiRightBound = screen.getGuiLeft() + screen.getXSize(); + int guiTopBound = screen.getGuiTop(); int firstTabX; - int tabY; + int firstTabY; + int controlX; + int controlY; + int controlWidth; + int controlHeight; + boolean vertical; - switch (dockSettings.dockSide()) { + switch (settings.dockSide()) { case TOP -> { - firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; - tabY = topBound - tabHeight + dockSettings.yOffset(); + controlWidth = baseControlWidth; + controlHeight = baseControlHeight; + firstTabX = inventoryLeftBound + TAB_X_OFFSET + settings.xOffset() + controlWidth; + firstTabY = guiTopBound - tabHeight + 1 + settings.yOffset(); + controlX = firstTabX - controlWidth - 2; + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + vertical = false; } case LEFT -> { - firstTabX = leftBound - (bagCount * tabWidth) + dockSettings.xOffset() + controlStripWidth - controlStripWidth; - tabY = bottomBound - tabHeight + dockSettings.yOffset(); + controlWidth = baseControlHeight; + controlHeight = baseControlWidth; + firstTabX = guiLeftBound - tabWidth + 1 + settings.xOffset(); + controlX = firstTabX + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = guiTopBound + 1 + settings.yOffset(); + firstTabY = controlY + controlHeight; + vertical = true; } case RIGHT -> { - firstTabX = rightBound + dockSettings.xOffset() + controlStripWidth; - tabY = bottomBound - tabHeight + dockSettings.yOffset(); + controlWidth = baseControlHeight; + controlHeight = baseControlWidth; + firstTabX = guiRightBound - 1 + settings.xOffset(); + controlX = firstTabX + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = guiTopBound + 1 + settings.yOffset(); + firstTabY = controlY + controlHeight; + vertical = true; } - case FLOATING -> { - int totalWidth = (bagCount * tabWidth) + controlStripWidth; - int controlX = (screen.width / 2) - (totalWidth / 2) + dockSettings.xOffset(); - firstTabX = controlX + controlStripWidth; - tabY = (screen.height / 2) + dockSettings.yOffset(); + case SCREEN_BOTTOM -> { + controlWidth = baseControlWidth; + controlHeight = baseControlHeight; + firstTabX = controlWidth + 2 + settings.xOffset(); + firstTabY = screen.height - tabHeight + settings.yOffset(); + controlX = settings.xOffset(); + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + vertical = false; + } + case SCREEN_TOP -> { + controlWidth = baseControlWidth; + controlHeight = baseControlHeight; + firstTabX = controlWidth + 2 + settings.xOffset(); + firstTabY = settings.yOffset(); + controlX = settings.xOffset(); + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + vertical = false; + } + case SCREEN_LEFT -> { + controlWidth = baseControlHeight; + controlHeight = baseControlWidth; + firstTabX = settings.xOffset(); + controlX = firstTabX + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = settings.yOffset(); + firstTabY = controlY + controlHeight; + vertical = true; + } + case SCREEN_RIGHT -> { + controlWidth = baseControlHeight; + controlHeight = baseControlWidth; + firstTabX = screen.width - tabWidth + settings.xOffset(); + controlX = firstTabX + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = settings.yOffset(); + firstTabY = controlY + controlHeight; + vertical = true; + } + case FLOATING_HORIZONTAL -> { + controlWidth = baseControlWidth; + controlHeight = baseControlHeight; + int totalWidth = (Math.min(bagCount, settings.maxTabs()) * tabWidth) + controlWidth; + controlX = (screen.width / 2) - (totalWidth / 2) + settings.xOffset(); + firstTabX = controlX + controlWidth + 2; + firstTabY = (screen.height / 2) + settings.yOffset(); + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + vertical = false; + } + case FLOATING_VERTICAL -> { + controlWidth = baseControlHeight; + controlHeight = baseControlWidth; + int totalHeight = (Math.min(bagCount, settings.maxTabs()) * tabHeight) + controlHeight; + controlX = (screen.width / 2) - (tabWidth / 2) + settings.xOffset() + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = (screen.height / 2) - (totalHeight / 2) + settings.yOffset(); + firstTabX = (screen.width / 2) - (tabWidth / 2) + settings.xOffset(); + firstTabY = controlY + controlHeight + 2; + vertical = true; } case BOTTOM -> { - firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; - tabY = bottomBound + TAB_Y_OFFSET + dockSettings.yOffset() + 8; + controlWidth = baseControlWidth; + controlHeight = baseControlHeight; + firstTabX = inventoryLeftBound + TAB_X_OFFSET + settings.xOffset() + controlWidth; + firstTabY = bottomBound + TAB_Y_OFFSET + settings.yOffset() + 8; + controlX = firstTabX - controlWidth - 2; + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + vertical = false; } - default -> { - firstTabX = leftBound + TAB_X_OFFSET + dockSettings.xOffset() + controlStripWidth; - tabY = bottomBound + TAB_Y_OFFSET + dockSettings.yOffset() + 8; + default -> throw new IllegalStateException("Unexpected dock side"); + } + + if (settings.dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL) { + int totalWidth = (Math.min(bagCount, settings.maxTabs()) * tabWidth) + controlWidth + 2; + int minControlX = 0; + int maxControlX = Math.max(0, screen.width - totalWidth); + int minTabY = 0; + int maxTabY = Math.max(0, screen.height - tabHeight); + int clampedControlX = clamp(controlX, minControlX, maxControlX); + int clampedFirstTabY = clamp(firstTabY, minTabY, maxTabY); + if (clampedControlX != controlX || clampedFirstTabY != firstTabY) { + int adjustedXOffset = settings.xOffset() + (clampedControlX - controlX); + int adjustedYOffset = settings.yOffset() + (clampedFirstTabY - firstTabY); + DockConfigManager.setEffectiveOffsets(getScreenKey(screen), adjustedXOffset, adjustedYOffset); + controlX = clampedControlX; + firstTabX = controlX + controlWidth + 2; + firstTabY = clampedFirstTabY; + controlY = firstTabY + Math.max(0, (tabHeight - controlHeight) / 2); + } + } else if (settings.dockSide() == DockConfigManager.DockSide.FLOATING_VERTICAL) { + int totalHeight = (Math.min(bagCount, settings.maxTabs()) * tabHeight) + controlHeight + 2; + int minTabX = 0; + int maxTabX = Math.max(0, screen.width - tabWidth); + int minControlY = 0; + int maxControlY = Math.max(0, screen.height - totalHeight); + int clampedFirstTabX = clamp(firstTabX, minTabX, maxTabX); + int clampedControlY = clamp(controlY, minControlY, maxControlY); + if (clampedFirstTabX != firstTabX || clampedControlY != controlY) { + int adjustedXOffset = settings.xOffset() + (clampedFirstTabX - firstTabX); + int adjustedYOffset = settings.yOffset() + (clampedControlY - controlY); + DockConfigManager.setEffectiveOffsets(getScreenKey(screen), adjustedXOffset, adjustedYOffset); + firstTabX = clampedFirstTabX; + controlX = firstTabX + Math.max(0, (tabWidth - controlWidth) / 2); + controlY = clampedControlY; + firstTabY = controlY + controlHeight + 2; } } - return new DockLayout(firstTabX, tabY, tabWidth, tabHeight, controlWidth, controlHeight, scale); + return new DockLayout(firstTabX, firstTabY, tabWidth, tabHeight, controlX, controlY, controlWidth, controlHeight, scale, settings.dockSide(), vertical, settings.maxTabs(), settings.dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL || settings.dockSide() == DockConfigManager.DockSide.FLOATING_VERTICAL); } private static DockControl getDockControl(DockLayout layout) { - int y = layout.tabY() + Math.max(0, (layout.tabHeight() - layout.controlHeight()) / 2); - int x = layout.firstTabX() - layout.controlWidth() - 2; - return new DockControl(x, y, layout.controlWidth(), layout.controlHeight(), layout.scale()); + return new DockControl(layout.controlX(), layout.controlY(), layout.controlWidth(), layout.controlHeight(), layout.scale(), layout.dockSide()); } - private static void renderDockControl(GuiGraphics guiGraphics, DockControl control, int mouseX, int mouseY) { - DockIcon hoveredIcon = control.hoveredIcon(mouseX, mouseY); - blitScaled( - guiGraphics, - DOCK_TEXTURE, - control.x(), - control.y(), - control.width(), - control.height(), - 0, - 0, - BASE_DOCK_WIDTH, - BASE_DOCK_HEIGHT, - BASE_DOCK_WIDTH, - BASE_DOCK_HEIGHT - ); - - if (hoveredIcon != null) { - if (hoveredIcon == DockIcon.CONFIG) { - guiGraphics.fill(control.x() + 2, control.y() + 1, control.x() + control.width() - 2, control.y() + (control.height() / 2), 0x22FFFFFF); + private static void renderDockControl(GuiGraphics g, DockControl control, int mouseX, int mouseY) { + DockIcon hovered = control.hoveredIcon(mouseX, mouseY); + blitRotated(g, DOCK_TEXTURE, control.x(), control.y(), control.width(), control.height(), 0, 0, BASE_DOCK_WIDTH, BASE_DOCK_HEIGHT, BASE_DOCK_WIDTH, BASE_DOCK_HEIGHT, control.rotationDegrees()); + if (hovered != null) { + if (control.horizontalButtons()) { + if (hovered == DockIcon.CONFIG) { + g.fill(control.x() + 1, control.y() + 2, control.x() + (control.width() / 2), control.y() + control.height() - 2, 0x22FFFFFF); + } else { + g.fill(control.x() + (control.width() / 2), control.y() + 2, control.x() + control.width() - 1, control.y() + control.height() - 2, 0x22FFFFFF); + } + } else if (hovered == DockIcon.CONFIG) { + g.fill(control.x() + 2, control.y() + 1, control.x() + control.width() - 2, control.y() + (control.height() / 2), 0x22FFFFFF); } else { - guiGraphics.fill(control.x() + 2, control.y() + (control.height() / 2), control.x() + control.width() - 2, control.y() + control.height() - 3, 0x22FFFFFF); + g.fill(control.x() + 2, control.y() + (control.height() / 2), control.x() + control.width() - 2, control.y() + control.height() - 3, 0x22FFFFFF); } } - int iconSize = Math.max(6, Math.min(Math.round(8 * control.scale()), control.width() - 4)); - int iconX = control.x() + (control.width() - iconSize) / 2; - int topSectionHeight = control.height() / 2; - int bottomSectionHeight = control.height() - topSectionHeight; - int gearY = control.y() + Math.max(1, (topSectionHeight - iconSize) / 2); - int lockY = control.y() + topSectionHeight + Math.max(1, (bottomSectionHeight - iconSize) / 2); - blitScaled(guiGraphics, GEAR_ICON_TEXTURE, iconX, gearY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); - blitScaled(guiGraphics, DockConfigManager.isInteractionsLocked() ? UNLOCK_ICON_TEXTURE : LOCK_ICON_TEXTURE, iconX, lockY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + int iconSize = Math.max(6, Math.min(Math.round(8 * control.scale()), Math.min(control.width(), control.height()) - 4)); + if (control.horizontalButtons()) { + int leftWidth = control.width() / 2; + int rightWidth = control.width() - leftWidth; + int iconY = control.y() + (control.height() - iconSize) / 2; + int gearX = control.x() + Math.max(1, (leftWidth - iconSize) / 2); + int lockX = control.x() + leftWidth + Math.max(1, (rightWidth - iconSize) / 2); + blitScaled(g, GEAR_ICON_TEXTURE, gearX, iconY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + blitScaled(g, DockConfigManager.isInteractionsLocked() ? UNLOCK_ICON_TEXTURE : LOCK_ICON_TEXTURE, lockX, iconY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + } else { + int iconX = control.x() + (control.width() - iconSize) / 2; + int topHeight = control.height() / 2; + int bottomHeight = control.height() - topHeight; + int gearY = control.y() + Math.max(1, (topHeight - iconSize) / 2); + int lockY = control.y() + topHeight + Math.max(1, (bottomHeight - iconSize) / 2); + blitScaled(g, GEAR_ICON_TEXTURE, iconX, gearY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + blitScaled(g, DockConfigManager.isInteractionsLocked() ? UNLOCK_ICON_TEXTURE : LOCK_ICON_TEXTURE, iconX, lockY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + } } private static Component getDockTooltip(DockIcon icon) { @@ -651,42 +747,53 @@ public final class BagTabOverlay { }; } - private static void blitScaled( - GuiGraphics guiGraphics, - ResourceLocation texture, - int x, - int y, - int width, - int height, - int u, - int v, - int regionWidth, - int regionHeight, - int textureWidth, - int textureHeight - ) { - guiGraphics.pose().pushPose(); - guiGraphics.pose().translate(x, y, 0.0F); - guiGraphics.pose().scale(width / (float) regionWidth, height / (float) regionHeight, 1.0F); - guiGraphics.blit(texture, 0, 0, u, v, regionWidth, regionHeight, textureWidth, textureHeight); - guiGraphics.pose().popPose(); + private static void blitScaled(GuiGraphics g, ResourceLocation texture, int x, int y, int width, int height, int u, int v, int regionWidth, int regionHeight, int textureWidth, int textureHeight) { + g.pose().pushPose(); + g.pose().translate(x, y, 0.0F); + g.pose().scale(width / (float) regionWidth, height / (float) regionHeight, 1.0F); + g.blit(texture, 0, 0, u, v, regionWidth, regionHeight, textureWidth, textureHeight); + g.pose().popPose(); + } + + private static void blitRotated(GuiGraphics g, ResourceLocation texture, int x, int y, int width, int height, int u, int v, int regionWidth, int regionHeight, int textureWidth, int textureHeight, float rotationDegrees) { + boolean quarterTurn = Math.abs(Math.round(rotationDegrees)) % 180 == 90; + int rotatedWidth = quarterTurn ? regionHeight : regionWidth; + int rotatedHeight = quarterTurn ? regionWidth : regionHeight; + g.pose().pushPose(); + g.pose().translate(x + (width / 2.0F), y + (height / 2.0F), 0.0F); + g.pose().mulPose(Axis.ZP.rotationDegrees(rotationDegrees)); + g.pose().scale(width / (float) rotatedWidth, height / (float) rotatedHeight, 1.0F); + g.blit(texture, -regionWidth / 2, -regionHeight / 2, u, v, regionWidth, regionHeight, textureWidth, textureHeight); + g.pose().popPose(); + } + + private static void pushTabTransform(GuiGraphics g, RenderedTab tab) { + g.pose().translate(tab.x() + (tab.width() / 2.0F), tab.y() + (tab.height() / 2.0F), 0.0F); + g.pose().mulPose(Axis.ZP.rotationDegrees(tab.rotationDegrees())); + g.pose().scale(tab.width() / (float) BASE_TAB_WIDTH, tab.height() / (float) BASE_TAB_HEIGHT, 1.0F); + g.pose().translate(-(BASE_TAB_WIDTH / 2.0F), -(BASE_TAB_HEIGHT / 2.0F), 0.0F); } private static int getInventoryTopBound(AbstractContainerScreen screen, Player player) { - return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream() - .mapToInt(slot -> slot.y) - .min() - .orElse(84); + return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream().mapToInt(slot -> slot.y).min().orElse(84); } private static int getInventoryBottomBound(AbstractContainerScreen screen, Player player) { - return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream() - .mapToInt(slot -> slot.y + 16) - .max() - .orElse(screen.getYSize()); + return screen.getGuiTop() + getPlayerInventorySlots(screen, player).stream().mapToInt(slot -> slot.y + 16).max().orElse(screen.getYSize()); } - private static int getMaxTabX(AbstractContainerScreen screen, Player player, DockLayout layout) { + private static int getMaxPrimary(AbstractContainerScreen screen, Player player, DockLayout layout) { + if (layout.vertical()) { + if (layout.dockSide() == DockConfigManager.DockSide.SCREEN_LEFT || layout.dockSide() == DockConfigManager.DockSide.SCREEN_RIGHT) { + return screen.height - layout.tabHeight(); + } + int guiBottom = screen.getGuiTop() + screen.getYSize(); + int inventoryBottom = getInventoryBottomBound(screen, player); + return Math.min(guiBottom, inventoryBottom) - layout.tabHeight(); + } + if (layout.dockSide() == DockConfigManager.DockSide.SCREEN_BOTTOM || layout.dockSide() == DockConfigManager.DockSide.SCREEN_TOP) { + return screen.width - layout.tabWidth(); + } int guiRight = screen.getGuiLeft() + screen.getXSize(); int inventoryRight = getInventoryRightBound(screen, player); return Math.min(guiRight, inventoryRight) - layout.tabWidth(); @@ -696,41 +803,116 @@ public final class BagTabOverlay { return screen.getClass().getName(); } - private record RenderedTab(BagEntry entry, int x, int y, boolean selected, boolean pinned, int itemX, int itemY, int width, int height) { + private static ClientTooltipPositioner createTooltipPositioner(AbstractContainerScreen screen) { + return new MenuTooltipPositioner(new ScreenRectangle(screen.getGuiLeft(), screen.getGuiTop(), screen.getXSize(), screen.getYSize())); + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private record RenderedTab(BagEntry entry, int x, int y, boolean selected, boolean pinned, DockConfigManager.DockSide dockSide, float scale, int width, int height) { private boolean isHovered(double mouseX, double mouseY) { return mouseX >= this.x && mouseX < this.x + this.width && mouseY >= this.y && mouseY < this.y + this.height; } - - private double centerX() { - return this.x + (this.width / 2.0D); + private double primaryCenter() { + return this.isVertical() ? this.y + (this.height / 2.0D) : this.x + (this.width / 2.0D); } - private RenderedTab withPosition(int newX, int newY) { - return new RenderedTab(this.entry, newX, newY, this.selected, this.pinned, newX + (this.itemX - this.x), newY + (this.itemY - this.y), this.width, this.height); + return new RenderedTab(this.entry, newX, newY, this.selected, this.pinned, this.dockSide, this.scale, this.width, this.height); + } + private boolean isVertical() { + return this.dockSide == DockConfigManager.DockSide.LEFT + || this.dockSide == DockConfigManager.DockSide.RIGHT + || this.dockSide == DockConfigManager.DockSide.SCREEN_LEFT + || this.dockSide == DockConfigManager.DockSide.SCREEN_RIGHT + || this.dockSide == DockConfigManager.DockSide.FLOATING_VERTICAL; + } + private float rotationDegrees() { + return switch (this.dockSide) { + case TOP -> 180.0F; + case LEFT -> 90.0F; + case RIGHT -> -90.0F; + case SCREEN_BOTTOM -> 180.0F; + case SCREEN_TOP -> 0.0F; + case SCREEN_LEFT -> -90.0F; + case SCREEN_RIGHT -> 90.0F; + case FLOATING_VERTICAL -> 90.0F; + default -> 0.0F; + }; + } + private boolean usesFloatingArt() { + return this.dockSide == DockConfigManager.DockSide.FLOATING_HORIZONTAL + || this.dockSide == DockConfigManager.DockSide.FLOATING_VERTICAL; + } + private ScreenPoint transformedPoint(int localX, int localY) { + double radians = Math.toRadians(this.rotationDegrees()); + double centerX = this.x + (this.width / 2.0D); + double centerY = this.y + (this.height / 2.0D); + double iconCenterX = localX + 8.0D; + double iconCenterY = localY + 8.0D; + double dx = iconCenterX - (this.width / 2.0D); + double dy = iconCenterY - (this.height / 2.0D); + int rotatedCenterX = (int) Math.round(centerX + (dx * Math.cos(radians)) - (dy * Math.sin(radians))); + int rotatedCenterY = (int) Math.round(centerY + (dx * Math.sin(radians)) + (dy * Math.cos(radians))); + return new ScreenPoint(rotatedCenterX - 8, rotatedCenterY - 8); + } + private int itemX() { + return transformedPoint(Math.max(2, Math.round(this.scale * 3.0F)), Math.max(1, Math.round(this.scale * 2.0F))).x(); + } + private int itemY() { + return transformedPoint(Math.max(2, Math.round(this.scale * 3.0F)), Math.max(1, Math.round(this.scale * 2.0F))).y(); } } private record PendingClick(int slot, double grabOffsetX, double grabOffsetY, boolean pinned) { } - private record DragState(int draggedSlot, double grabOffsetX, List previewOrder) { + private record DragState(int draggedSlot, double grabOffsetX, double grabOffsetY, List previewOrder) { } - private record DockLayout(int firstTabX, int tabY, int tabWidth, int tabHeight, int controlWidth, int controlHeight, float scale) { + 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) { } - private record DockControl(int x, int y, int width, int height, float scale) { + private record PendingCursorRestore(double guiX, double guiY, long expiresAt) { + } + + private record DockControl(int x, int y, int width, int height, float scale, DockConfigManager.DockSide dockSide) { private DockIcon hoveredIcon(double mouseX, double mouseY) { if (mouseX < this.x || mouseX >= this.x + this.width || mouseY < this.y || mouseY >= this.y + this.height) { return null; } - int split = this.y + (this.height / 2); - return mouseY < split ? DockIcon.CONFIG : DockIcon.LOCK; + if (horizontalButtons()) { + return mouseX < this.x + (this.width / 2) ? DockIcon.CONFIG : DockIcon.LOCK; + } + return mouseY < this.y + (this.height / 2) ? DockIcon.CONFIG : DockIcon.LOCK; } + private boolean horizontalButtons() { + return this.dockSide == DockConfigManager.DockSide.LEFT + || this.dockSide == DockConfigManager.DockSide.RIGHT + || this.dockSide == DockConfigManager.DockSide.SCREEN_LEFT + || this.dockSide == DockConfigManager.DockSide.SCREEN_RIGHT + || this.dockSide == DockConfigManager.DockSide.FLOATING_VERTICAL; + } + private float rotationDegrees() { + return switch (this.dockSide) { + case TOP -> 180.0F; + case LEFT -> 90.0F; + case RIGHT -> -90.0F; + case SCREEN_BOTTOM -> 180.0F; + case SCREEN_TOP -> 0.0F; + case SCREEN_LEFT -> -90.0F; + case SCREEN_RIGHT -> 90.0F; + default -> 0.0F; + }; + } + } + + private record ScreenPoint(int x, int y) { } private enum DockIcon { CONFIG, - LOCK, + LOCK } } diff --git a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java index 4c179d3..2586f32 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigManager.java @@ -52,6 +52,17 @@ public final class DockConfigManager { save(); } + public static void setEffectiveOffsets(String screenKey, int xOffset, int yOffset) { + SessionSettings settings = getSessionSettings(); + if (settings.screenOverrides.containsKey(screenKey)) { + DockSettings override = settings.screenOverrides.get(screenKey); + settings.screenOverrides.put(screenKey, override.withXOffset(xOffset).withYOffset(yOffset)); + } else { + settings.globalDefault = settings.globalDefault.withXOffset(xOffset).withYOffset(yOffset); + } + save(); + } + public static void clearOverride(String screenKey) { SessionSettings settings = getSessionSettings(); settings.screenOverrides.remove(screenKey); @@ -149,19 +160,29 @@ public final class DockConfigManager { TOP, LEFT, RIGHT, - FLOATING; + SCREEN_BOTTOM, + SCREEN_TOP, + SCREEN_LEFT, + SCREEN_RIGHT, + FLOATING_HORIZONTAL, + FLOATING_VERTICAL; public DockSide next() { DockSide[] values = values(); return values[(ordinal() + 1) % values.length]; } + + public DockSide previous() { + DockSide[] values = values(); + return values[(ordinal() - 1 + values.length) % values.length]; + } } public static final class DockSettings { private DockSide dockSide = DockSide.BOTTOM; private int xOffset = 0; private int yOffset = 0; - private int scalePercent = 100; + private int maxTabs = 8; public DockSide dockSide() { return dockSide; @@ -175,8 +196,8 @@ public final class DockConfigManager { return yOffset; } - public int scalePercent() { - return scalePercent; + public int maxTabs() { + return maxTabs; } public DockSettings withDockSide(DockSide nextDockSide) { @@ -197,9 +218,9 @@ public final class DockConfigManager { return copy; } - public DockSettings withScalePercent(int nextScalePercent) { + public DockSettings withMaxTabs(int nextMaxTabs) { DockSettings copy = copy(); - copy.scalePercent = Math.max(75, Math.min(150, nextScalePercent)); + copy.maxTabs = Math.max(1, Math.min(20, nextMaxTabs)); return copy; } @@ -208,9 +229,17 @@ public final class DockConfigManager { copy.dockSide = this.dockSide; copy.xOffset = this.xOffset; copy.yOffset = this.yOffset; - copy.scalePercent = this.scalePercent; + copy.maxTabs = this.maxTabs; return copy; } + + private DockSettings normalize() { + if (dockSide == null) { + dockSide = DockSide.FLOATING_HORIZONTAL; + } + maxTabs = Math.max(1, Math.min(20, maxTabs)); + return this; + } } private static final class SessionSettings { @@ -223,8 +252,11 @@ public final class DockConfigManager { if (globalDefault == null) { globalDefault = new DockSettings(); } + globalDefault = globalDefault.normalize(); if (screenOverrides == null) { screenOverrides = new HashMap<>(); + } else { + screenOverrides.replaceAll((key, value) -> value == null ? new DockSettings() : value.normalize()); } if (rememberedPages == null) { rememberedPages = new HashMap<>(); diff --git a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java index 224f4f7..ac90f74 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/DockConfigScreen.java @@ -4,6 +4,7 @@ import com.trunksbomb.bagtabs.BagTabs; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; @@ -19,8 +20,8 @@ public class DockConfigScreen extends Screen { private Button xPlusButton; private Button yMinusButton; private Button yPlusButton; - private Button scaleMinusButton; - private Button scalePlusButton; + private Button maxTabsMinusButton; + private Button maxTabsPlusButton; private Button resetButton; public DockConfigScreen(Screen parent) { @@ -49,19 +50,19 @@ public class DockConfigScreen extends Screen { syncLabels(); }).bounds(centerX - 90, top + 26, 180, 20).build()); - this.xMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustX(-4)) + this.xMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustX(-getOffsetStep())) .bounds(centerX - 90, top + 56, 20, 20).build()); - this.xPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustX(4)) + this.xPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustX(getOffsetStep())) .bounds(centerX + 70, top + 56, 20, 20).build()); - this.yMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustY(-4)) + this.yMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustY(-getOffsetStep())) .bounds(centerX - 90, top + 82, 20, 20).build()); - this.yPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustY(4)) + this.yPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustY(getOffsetStep())) .bounds(centerX + 70, top + 82, 20, 20).build()); - this.scaleMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustScale(-5)) + this.maxTabsMinusButton = this.addRenderableWidget(Button.builder(Component.literal("-"), button -> adjustMaxTabs(-1)) .bounds(centerX - 90, top + 108, 20, 20).build()); - this.scalePlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustScale(5)) + this.maxTabsPlusButton = this.addRenderableWidget(Button.builder(Component.literal("+"), button -> adjustMaxTabs(1)) .bounds(centerX + 70, top + 108, 20, 20).build()); this.resetButton = this.addRenderableWidget(Button.builder(BagTabs.translation("dock.reset"), button -> { @@ -75,6 +76,11 @@ public class DockConfigScreen extends Screen { .bounds(centerX - 90, top + 164, 180, 20).build()); syncLabels(); + Tooltip offsetTooltip = Tooltip.create(BagTabs.translation("dock.offset_steps")); + this.xMinusButton.setTooltip(offsetTooltip); + this.xPlusButton.setTooltip(offsetTooltip); + this.yMinusButton.setTooltip(offsetTooltip); + this.yPlusButton.setTooltip(offsetTooltip); } @Override @@ -87,7 +93,7 @@ public class DockConfigScreen extends Screen { guiGraphics.drawCenteredString(this.font, this.title, centerX, top - 18, 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("Tab Size: " + this.dockSettings.scalePercent() + "%"), centerX, top + 114, 0xFFFFFF); + guiGraphics.drawCenteredString(this.font, Component.literal("Max Tabs: " + this.dockSettings.maxTabs()), centerX, top + 114, 0xFFFFFF); } @Override @@ -96,6 +102,17 @@ public class DockConfigScreen extends Screen { Minecraft.getInstance().setScreen(this.parent); } + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 1 && this.sideButton != null && this.sideButton.isMouseOver(mouseX, mouseY)) { + this.dockSettings = this.dockSettings.withDockSide(this.dockSettings.dockSide().previous()); + saveCurrent(); + syncLabels(); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + private void adjustX(int delta) { this.dockSettings = this.dockSettings.withXOffset(this.dockSettings.xOffset() + delta); saveCurrent(); @@ -108,12 +125,27 @@ public class DockConfigScreen extends Screen { syncLabels(); } - private void adjustScale(int delta) { - this.dockSettings = this.dockSettings.withScalePercent(this.dockSettings.scalePercent() + delta); + private void adjustMaxTabs(int delta) { + this.dockSettings = this.dockSettings.withMaxTabs(this.dockSettings.maxTabs() + delta); saveCurrent(); syncLabels(); } + private int getOffsetStep() { + boolean shift = hasShiftDown(); + boolean control = hasControlDown(); + if (shift && control) { + return 100; + } + if (control) { + return 25; + } + if (shift) { + return 5; + } + return 1; + } + private void syncLabels() { this.targetButton.setMessage(this.editOverride ? BagTabs.translation("dock.target.override") diff --git a/src/main/resources/assets/bagtabs/lang/en_us.json b/src/main/resources/assets/bagtabs/lang/en_us.json index 02fc7e7..ed14240 100644 --- a/src/main/resources/assets/bagtabs/lang/en_us.json +++ b/src/main/resources/assets/bagtabs/lang/en_us.json @@ -15,13 +15,19 @@ "bagtabs.dock.target.override": "Editing: This Screen Override", "bagtabs.dock.x_offset": "X Offset", "bagtabs.dock.y_offset": "Y Offset", - "bagtabs.dock.scale": "Tab Size", + "bagtabs.dock.max_tabs": "Max Tabs", "bagtabs.dock.open": "Open dock settings", "bagtabs.dock.lock": "Lock tab interactions", "bagtabs.dock.unlock": "Unlock tab interactions", + "bagtabs.dock.offset_steps": "Click: 1, Shift: 5, Ctrl: 25, Ctrl+Shift: 100", "bagtabs.dock.side.bottom": "Dock: Bottom", "bagtabs.dock.side.top": "Dock: Top", "bagtabs.dock.side.left": "Dock: Left", "bagtabs.dock.side.right": "Dock: Right", - "bagtabs.dock.side.floating": "Dock: Floating" + "bagtabs.dock.side.screen_bottom": "Dock: Screen Bottom", + "bagtabs.dock.side.screen_top": "Dock: Screen Top", + "bagtabs.dock.side.screen_left": "Dock: Screen Left", + "bagtabs.dock.side.screen_right": "Dock: Screen Right", + "bagtabs.dock.side.floating_horizontal": "Dock: Floating Horizontal", + "bagtabs.dock.side.floating_vertical": "Dock: Floating Vertical" }