From 56fcb49f4d978ea31ae3c8a44598ddc13fd3277f Mon Sep 17 00:00:00 2001 From: trunksbomb Date: Mon, 23 Mar 2026 00:41:46 -0400 Subject: [PATCH] UI polishes Implement scrolling tabs --- .../bagtabs/client/BagTabOverlay.java | 324 +++++++++++++++--- .../resources/assets/bagtabs/lang/en_us.json | 2 + .../assets/bagtabs/textures/gui/dock_next.png | Bin 0 -> 491 bytes .../assets/bagtabs/textures/gui/dock_prev.png | Bin 0 -> 496 bytes 4 files changed, 284 insertions(+), 42 deletions(-) create mode 100644 src/main/resources/assets/bagtabs/textures/gui/dock_next.png create mode 100644 src/main/resources/assets/bagtabs/textures/gui/dock_prev.png diff --git a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java index 03dc04c..b53e805 100644 --- a/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java +++ b/src/main/java/com/trunksbomb/bagtabs/client/BagTabOverlay.java @@ -44,6 +44,8 @@ public final class BagTabOverlay { private static final ResourceLocation LOCK_ICON_TEXTURE = BagTabs.id("textures/gui/dock_lock.png"); private static final ResourceLocation UNLOCK_ICON_TEXTURE = BagTabs.id("textures/gui/dock_unlock.png"); 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 int BASE_TAB_WIDTH = 22; private static final int BASE_TAB_HEIGHT = 22; private static final int BASE_DOCK_WIDTH = 18; @@ -60,6 +62,7 @@ public final class BagTabOverlay { private static PendingClick pendingClick; private static DragState dragState; private static PendingCursorRestore pendingCursorRestore; + private static long nextAutoScrollAt; private BagTabOverlay() { } @@ -74,8 +77,8 @@ public final class BagTabOverlay { return; } - List tabs = getRenderedTabs(screen, player); - if (tabs.isEmpty()) { + TabStrip strip = getTabStrip(screen, player); + if (strip.tabs().isEmpty()) { return; } @@ -83,12 +86,10 @@ public final class BagTabOverlay { int mouseX = event.getMouseX(); int mouseY = event.getMouseY(); ItemStack carried = screen.getMenu().getCarried(); - DockLayout layout = getDockLayout(screen, player, tabs.size()); - DockControl control = getDockControl(layout); refreshInsertTargets(screen, carried); - RenderedTab dragged = null; - for (RenderedTab tab : tabs) { + RenderedTab dragged = getDraggedTab(strip); + for (RenderedTab tab : strip.tabs()) { if (dragState != null && tab.entry().slot() == dragState.draggedSlot()) { dragged = tab; continue; @@ -98,14 +99,16 @@ public final class BagTabOverlay { renderPinOverlay(g, tab); } - if (layout.dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL - || layout.dockSide() == DockConfigManager.DockSide.FLOATING_VERTICAL) { + if (strip.layout().dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL + || strip.layout().dockSide() == DockConfigManager.DockSide.FLOATING_VERTICAL) { g.pose().pushPose(); g.pose().translate(0.0F, 0.0F, 150.0F); - renderDockControl(g, control, mouseX, mouseY); + renderDockControl(g, strip.control(), mouseX, mouseY); + renderScrollControl(g, strip.scrollControl(), mouseX, mouseY); g.pose().popPose(); } else { - renderDockControl(g, control, mouseX, mouseY); + renderDockControl(g, strip.control(), mouseX, mouseY); + renderScrollControl(g, strip.scrollControl(), mouseX, mouseY); } if (dragged != null) { @@ -124,17 +127,23 @@ public final class BagTabOverlay { return; } - DockIcon hovered = control.hoveredIcon(mouseX, mouseY); + DockIcon hovered = strip.control().hoveredIcon(mouseX, mouseY); if (hovered != null) { - g.renderTooltip(Minecraft.getInstance().font, List.of(getDockTooltip(hovered).getVisualOrderText()), createTooltipPositioner(screen), mouseX, mouseY); + renderManagedTooltip(g, screen, List.of(getDockTooltip(hovered).getVisualOrderText()), mouseX, mouseY); return; } - for (RenderedTab tab : tabs) { + ScrollIcon hoveredScroll = hoveredScrollIcon(strip.scrollControl(), mouseX, mouseY); + if (hoveredScroll != null) { + renderManagedTooltip(g, screen, List.of(getScrollTooltip(hoveredScroll).getVisualOrderText()), mouseX, mouseY); + return; + } + + for (RenderedTab tab : strip.tabs()) { if (!tab.isHovered(mouseX, mouseY)) { continue; } - g.renderTooltip(Minecraft.getInstance().font, getTooltipLines(tab, tabs).stream().map(Component::getVisualOrderText).toList(), createTooltipPositioner(screen), mouseX, mouseY); + renderManagedTooltip(g, screen, getTooltipLines(tab, strip.allEntries()).stream().map(Component::getVisualOrderText).toList(), mouseX, mouseY); break; } } @@ -148,10 +157,9 @@ public final class BagTabOverlay { return; } - List tabs = getRenderedTabs(screen, player); - DockControl control = getDockControl(getDockLayout(screen, player, tabs.size())); + TabStrip strip = getTabStrip(screen, player); if (event.getButton() == 0) { - DockIcon clicked = control.hoveredIcon(event.getMouseX(), event.getMouseY()); + DockIcon clicked = strip.control().hoveredIcon(event.getMouseX(), event.getMouseY()); if (clicked == DockIcon.LOCK) { DockConfigManager.toggleInteractionsLocked(); dragState = null; @@ -166,17 +174,26 @@ public final class BagTabOverlay { event.setCanceled(true); return; } + + ScrollIcon scrollIcon = hoveredScrollIcon(strip.scrollControl(), event.getMouseX(), event.getMouseY()); + if (scrollIcon != null) { + int offset = strip.scrollOffset() + (scrollIcon == ScrollIcon.NEXT ? 1 : -1); + setScrollOffset(screen, offset, strip.maxScrollOffset()); + playScrollSound(); + event.setCanceled(true); + return; + } } if (event.getButton() == 1) { if (DockConfigManager.isInteractionsLocked()) { return; } - for (RenderedTab tab : tabs) { + for (RenderedTab tab : strip.tabs()) { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; } - TabPinManager.ToggleResult result = TabPinManager.togglePin(tab.entry(), tabs.stream().map(RenderedTab::entry).toList()); + TabPinManager.ToggleResult result = TabPinManager.togglePin(tab.entry(), strip.allEntries()); if (result.changed()) { Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); } else { @@ -191,7 +208,7 @@ public final class BagTabOverlay { if (event.getButton() != 0 || !screen.getMenu().getCarried().isEmpty()) { return; } - for (RenderedTab tab : tabs) { + for (RenderedTab tab : strip.tabs()) { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; } @@ -210,13 +227,14 @@ public final class BagTabOverlay { return; } - List tabs = getRenderedTabs(screen, player); + TabStrip strip = getTabStrip(screen, player); + List tabs = strip.tabs(); if (pendingClick != null && pendingClick.pinned()) { 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())); + dragState = new DragState(pendingClick.slot(), pendingClick.grabOffsetX(), pendingClick.grabOffsetY(), TabPinManager.getPinnedIdentityOrder(strip.allEntries())); pendingClick = null; } } @@ -225,6 +243,7 @@ public final class BagTabOverlay { return; } updateDragPreview(event.getMouseX(), event.getMouseY(), tabs); + maybeAutoScrollDuringDrag(screen, strip, event.getMouseX(), event.getMouseY()); event.setCanceled(true); } @@ -239,9 +258,10 @@ public final class BagTabOverlay { ItemStack carried = screen.getMenu().getCarried(); if (carried.isEmpty()) { - List tabs = getRenderedTabs(screen, player); + TabStrip strip = getTabStrip(screen, player); + List tabs = strip.tabs(); if (dragState != null) { - TabPinManager.applyPinnedOrder(dragState.previewOrder(), tabs.stream().map(RenderedTab::entry).toList()); + TabPinManager.applyPinnedOrder(dragState.previewOrder(), strip.allEntries()); Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); dragState = null; pendingClick = null; @@ -263,7 +283,7 @@ public final class BagTabOverlay { return; } - for (RenderedTab tab : getRenderedTabs(screen, player)) { + for (RenderedTab tab : getTabStrip(screen, player).tabs()) { if (!tab.isHovered(event.getMouseX(), event.getMouseY())) { continue; } @@ -307,26 +327,27 @@ public final class BagTabOverlay { pendingInsertRequestId = -1; } - private static List getRenderedTabs(AbstractContainerScreen screen, Player player) { + private static TabStrip getTabStrip(AbstractContainerScreen screen, Player player) { List bags = TabPinManager.sortTabs(BagAccess.findBags(player)); if (dragState != null) { bags = applyPreviewOrder(bags, dragState.previewOrder()); } DockLayout layout = getDockLayout(screen, player, bags.size()); + int visibleCapacityWithoutScroll = getVisibleCapacity(screen, player, layout, false); + boolean overflow = bags.size() > Math.min(layout.maxTabs(), visibleCapacityWithoutScroll); + int visibleCount = Math.min(bags.size(), Math.min(layout.maxTabs(), getVisibleCapacity(screen, player, layout, overflow))); + int maxScrollOffset = Math.max(0, bags.size() - visibleCount); + int scrollOffset = clamp(DockConfigManager.getRememberedPage(getScreenKey(screen)), 0, maxScrollOffset); + if (scrollOffset != DockConfigManager.getRememberedPage(getScreenKey(screen))) { + DockConfigManager.setRememberedPage(getScreenKey(screen), scrollOffset); + } + List rendered = new ArrayList<>(); int activeBagSlot = getActiveBagSlot(screen); int x = layout.firstTabX(); int y = layout.firstTabY(); - int maxPrimary = getMaxPrimary(screen, player, layout); - int renderedCount = 0; - for (BagEntry bag : bags) { - if (renderedCount >= layout.maxTabs()) { - break; - } - if (!layout.floating() && (layout.vertical() ? y : x) > maxPrimary) { - break; - } + for (BagEntry bag : bags.subList(scrollOffset, scrollOffset + visibleCount)) { 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; @@ -335,7 +356,10 @@ public final class BagTabOverlay { } renderedCount++; } - return rendered; + + DockControl control = getDockControl(layout); + ScrollControl scrollControl = overflow ? getScrollControl(layout, renderedCount) : null; + return new TabStrip(bags, rendered, layout, control, scrollControl, scrollOffset, maxScrollOffset); } private static List applyPreviewOrder(List bags, List previewOrder) { @@ -457,12 +481,42 @@ public final class BagTabOverlay { pendingCursorRestore = new PendingCursorRestore(guiX, guiY, Util.getMillis() + 1000L); } + private static void playScrollSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 0.65F)); + } + private static void expirePendingCursorRestore() { if (pendingCursorRestore != null && Util.getMillis() > pendingCursorRestore.expiresAt()) { pendingCursorRestore = null; } } + private static RenderedTab getDraggedTab(TabStrip strip) { + if (dragState == null) { + return null; + } + RenderedTab visibleDragged = strip.tabs().stream().filter(tab -> tab.entry().slot() == dragState.draggedSlot()).findFirst().orElse(null); + if (visibleDragged != null) { + return visibleDragged; + } + BagEntry draggedEntry = strip.allEntries().stream().filter(entry -> entry.slot() == dragState.draggedSlot()).findFirst().orElse(null); + if (draggedEntry == null) { + return null; + } + int activeBagSlot = strip.tabs().stream().filter(RenderedTab::selected).mapToInt(tab -> tab.entry().slot()).findFirst().orElse(Integer.MIN_VALUE); + return new RenderedTab( + draggedEntry, + strip.layout().firstTabX(), + strip.layout().firstTabY(), + draggedEntry.slot() == activeBagSlot, + TabPinManager.isPinned(draggedEntry, strip.allEntries()), + strip.layout().dockSide(), + strip.layout().scale(), + strip.layout().tabWidth(), + strip.layout().tabHeight() + ); + } + private static void renderPinOverlay(GuiGraphics g, RenderedTab tab) { if (!tab.pinned()) { return; @@ -474,7 +528,7 @@ public final class BagTabOverlay { g.fill(x + 2, y + 4, x + 3, y + 5, 0xFFD94A4A); } - private static List getTooltipLines(RenderedTab tab, List tabs) { + private static List getTooltipLines(RenderedTab tab, List tabs) { List lines = new ArrayList<>(); lines.add(tab.entry().stack().getHoverName()); lines.add(Component.literal("Left-click: open")); @@ -484,7 +538,7 @@ public final class BagTabOverlay { lines.add(Component.literal("Right-click: unpin")); } } else { - String failureReason = TabPinManager.getPinFailureReason(tab.entry(), tabs.stream().map(RenderedTab::entry).toList()); + String failureReason = TabPinManager.getPinFailureReason(tab.entry(), tabs); if (DockConfigManager.isInteractionsLocked()) { return lines; } @@ -543,6 +597,38 @@ public final class BagTabOverlay { } } + private static void maybeAutoScrollDuringDrag(AbstractContainerScreen screen, TabStrip strip, double mouseX, double mouseY) { + if (dragState == null || strip.scrollControl() == null || strip.tabs().isEmpty()) { + return; + } + long now = Util.getMillis(); + if (now < nextAutoScrollAt) { + return; + } + + RenderedTab firstTab = strip.tabs().getFirst(); + RenderedTab lastTab = strip.tabs().get(strip.tabs().size() - 1); + double primary = strip.layout().vertical() ? mouseY : mouseX; + double leadingThreshold = strip.layout().vertical() ? firstTab.y() + 4.0D : firstTab.x() + 4.0D; + double trailingThreshold = strip.layout().vertical() ? lastTab.y() + lastTab.height() - 4.0D : lastTab.x() + lastTab.width() - 4.0D; + + int dragScrollLimit = getPinnedDragScrollLimit(strip); + if (primary <= leadingThreshold && strip.scrollOffset() > 0) { + setScrollOffset(screen, strip.scrollOffset() - 1, dragScrollLimit); + playScrollSound(); + nextAutoScrollAt = now + 125L; + } else if (primary >= trailingThreshold && strip.scrollOffset() < dragScrollLimit) { + setScrollOffset(screen, strip.scrollOffset() + 1, dragScrollLimit); + playScrollSound(); + nextAutoScrollAt = now + 125L; + } + } + + private static int getPinnedDragScrollLimit(TabStrip strip) { + int pinnedCount = (int) strip.allEntries().stream().filter(entry -> TabPinManager.isPinned(entry, strip.allEntries())).count(); + return Math.max(0, pinnedCount - strip.tabs().size()); + } + private static DockLayout getDockLayout(AbstractContainerScreen screen, Player player, int bagCount) { DockConfigManager.DockSettings settings = DockConfigManager.getEffectiveSettings(getScreenKey(screen)); float scale = 1.0F; @@ -556,6 +642,8 @@ public final class BagTabOverlay { int guiLeftBound = screen.getGuiLeft(); int guiRightBound = screen.getGuiLeft() + screen.getXSize(); int guiTopBound = screen.getGuiTop(); + int visibleBagCount = Math.min(bagCount, settings.maxTabs()); + boolean floatingOverflow = bagCount > settings.maxTabs(); int firstTabX; int firstTabY; int controlX; @@ -631,7 +719,7 @@ public final class BagTabOverlay { case FLOATING_HORIZONTAL -> { controlWidth = baseControlWidth; controlHeight = baseControlHeight; - int totalWidth = (Math.min(bagCount, settings.maxTabs()) * tabWidth) + controlWidth; + int totalWidth = controlWidth + 2 + (visibleBagCount * tabWidth) + (floatingOverflow ? controlWidth + 2 : 0); controlX = (screen.width / 2) - (totalWidth / 2) + settings.xOffset(); firstTabX = controlX + controlWidth + 2; firstTabY = (screen.height / 2) + settings.yOffset(); @@ -641,7 +729,7 @@ public final class BagTabOverlay { case FLOATING_VERTICAL -> { controlWidth = baseControlHeight; controlHeight = baseControlWidth; - int totalHeight = (Math.min(bagCount, settings.maxTabs()) * tabHeight) + controlHeight; + int totalHeight = controlHeight + 2 + (visibleBagCount * tabHeight) + (floatingOverflow ? controlHeight + 2 : 0); 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(); @@ -661,7 +749,7 @@ public final class BagTabOverlay { } if (settings.dockSide() == DockConfigManager.DockSide.FLOATING_HORIZONTAL) { - int totalWidth = (Math.min(bagCount, settings.maxTabs()) * tabWidth) + controlWidth + 2; + int totalWidth = controlWidth + 2 + (visibleBagCount * tabWidth) + (floatingOverflow ? controlWidth + 2 : 0); int minControlX = 0; int maxControlX = Math.max(0, screen.width - totalWidth); int minTabY = 0; @@ -678,7 +766,7 @@ public final class BagTabOverlay { 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 totalHeight = controlHeight + 2 + (visibleBagCount * tabHeight) + (floatingOverflow ? controlHeight + 2 : 0); int minTabX = 0; int maxTabX = Math.max(0, screen.width - tabWidth); int minControlY = 0; @@ -703,6 +791,40 @@ public final class BagTabOverlay { return new DockControl(layout.controlX(), layout.controlY(), layout.controlWidth(), layout.controlHeight(), layout.scale(), layout.dockSide()); } + private static ScrollControl getScrollControl(DockLayout layout, int visibleCount) { + int primarySpan = visibleCount <= 0 ? 0 : visibleCount * (layout.vertical() ? layout.tabHeight() : layout.tabWidth()); + if (layout.vertical()) { + return new ScrollControl(layout.firstTabX() + Math.max(0, (layout.tabWidth() - layout.controlWidth()) / 2), + layout.firstTabY() + primarySpan + 2, + layout.controlWidth(), + layout.controlHeight(), + layout.scale(), + layout.dockSide()); + } + return new ScrollControl(layout.firstTabX() + primarySpan + 2, + layout.firstTabY() + Math.max(0, (layout.tabHeight() - layout.controlHeight()) / 2), + layout.controlWidth(), + layout.controlHeight(), + layout.scale(), + layout.dockSide()); + } + + private static int getVisibleCapacity(AbstractContainerScreen screen, Player player, DockLayout layout, boolean reserveScrollControl) { + if (layout.floating()) { + return Math.max(1, layout.maxTabs()); + } + int startPrimary = layout.vertical() ? layout.firstTabY() : layout.firstTabX(); + int maxPrimary = getMaxPrimary(screen, player, layout); + int tabPrimary = layout.vertical() ? layout.tabHeight() : layout.tabWidth(); + int availablePrimary = Math.max(0, maxPrimary - startPrimary + tabPrimary); + int reservedPrimary = reserveScrollControl ? (layout.vertical() ? layout.controlHeight() : layout.controlWidth()) + 2 : 0; + return Math.max(0, (availablePrimary - reservedPrimary) / tabPrimary); + } + + private static void setScrollOffset(AbstractContainerScreen screen, int scrollOffset, int maxScrollOffset) { + DockConfigManager.setRememberedPage(getScreenKey(screen), clamp(scrollOffset, 0, maxScrollOffset)); + } + 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()); @@ -740,6 +862,46 @@ public final class BagTabOverlay { } } + private static void renderScrollControl(GuiGraphics g, ScrollControl control, int mouseX, int mouseY) { + if (control == null) { + return; + } + ScrollIcon 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 == ScrollIcon.PREV) { + 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 == ScrollIcon.PREV) { + g.fill(control.x() + 2, control.y() + 1, control.x() + control.width() - 2, control.y() + (control.height() / 2), 0x22FFFFFF); + } else { + 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()), 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 prevX = control.x() + Math.max(1, (leftWidth - iconSize) / 2); + int nextX = control.x() + leftWidth + Math.max(1, (rightWidth - iconSize) / 2); + blitScaledRotated(g, PREV_ICON_TEXTURE, prevX, iconY, iconSize, 90.0F); + blitScaledRotated(g, NEXT_ICON_TEXTURE, nextX, iconY, iconSize, 90.0F); + } else { + int iconX = control.x() + (control.width() - iconSize) / 2; + int topHeight = control.height() / 2; + int bottomHeight = control.height() - topHeight; + int prevY = control.y() + Math.max(1, (topHeight - iconSize) / 2); + int nextY = control.y() + topHeight + Math.max(1, (bottomHeight - iconSize) / 2); + blitScaled(g, PREV_ICON_TEXTURE, iconX, prevY, iconSize, iconSize, 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + blitScaled(g, NEXT_ICON_TEXTURE, iconX, nextY, 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) { return switch (icon) { case CONFIG -> BagTabs.translation("dock.open"); @@ -747,6 +909,27 @@ public final class BagTabOverlay { }; } + private static Component getScrollTooltip(ScrollIcon icon) { + return switch (icon) { + case PREV -> BagTabs.translation("dock.scroll_prev"); + case NEXT -> BagTabs.translation("dock.scroll_next"); + }; + } + + private static ScrollIcon hoveredScrollIcon(ScrollControl control, double mouseX, double mouseY) { + return control == null ? null : control.hoveredIcon(mouseX, mouseY); + } + + private static void renderManagedTooltip(GuiGraphics g, AbstractContainerScreen screen, List lines, int mouseX, int mouseY) { + @SuppressWarnings("unchecked") + List formatted = (List) lines; + if (usesDefaultTooltipPlacement(screen)) { + g.renderTooltip(Minecraft.getInstance().font, formatted, mouseX, mouseY); + return; + } + g.renderTooltip(Minecraft.getInstance().font, formatted, createTooltipPositioner(screen), mouseX, mouseY); + } + 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); @@ -755,6 +938,15 @@ public final class BagTabOverlay { g.pose().popPose(); } + private static void blitScaledRotated(GuiGraphics g, ResourceLocation texture, int x, int y, int size, float rotationDegrees) { + g.pose().pushPose(); + g.pose().translate(x + (size / 2.0F), y + (size / 2.0F), 0.0F); + g.pose().mulPose(Axis.ZP.rotationDegrees(rotationDegrees)); + g.pose().scale(size / (float) BASE_DOCK_ICON_SIZE, size / (float) BASE_DOCK_ICON_SIZE, 1.0F); + g.blit(texture, -(BASE_DOCK_ICON_SIZE / 2), -(BASE_DOCK_ICON_SIZE / 2), 0, 0, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE, BASE_DOCK_ICON_SIZE); + 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; @@ -807,6 +999,13 @@ public final class BagTabOverlay { return new MenuTooltipPositioner(new ScreenRectangle(screen.getGuiLeft(), screen.getGuiTop(), screen.getXSize(), screen.getYSize())); } + private static boolean usesDefaultTooltipPlacement(AbstractContainerScreen screen) { + DockConfigManager.DockSide dockSide = DockConfigManager.getEffectiveSettings(getScreenKey(screen)).dockSide(); + return dockSide == DockConfigManager.DockSide.BOTTOM + || dockSide == DockConfigManager.DockSide.SCREEN_BOTTOM + || dockSide == DockConfigManager.DockSide.FLOATING_HORIZONTAL; + } + private static int clamp(int value, int min, int max) { return Math.max(min, Math.min(max, value)); } @@ -871,6 +1070,9 @@ public final class BagTabOverlay { private record DragState(int draggedSlot, double grabOffsetX, double grabOffsetY, List previewOrder) { } + private record TabStrip(List allEntries, List tabs, DockLayout layout, DockControl control, ScrollControl scrollControl, int scrollOffset, int maxScrollOffset) { + } + 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) { } @@ -908,6 +1110,39 @@ public final class BagTabOverlay { } } + private record ScrollControl(int x, int y, int width, int height, float scale, DockConfigManager.DockSide dockSide) { + private ScrollIcon hoveredIcon(double mouseX, double mouseY) { + if (mouseX < this.x || mouseX >= this.x + this.width || mouseY < this.y || mouseY >= this.y + this.height) { + return null; + } + if (horizontalButtons()) { + return mouseX < this.x + (this.width / 2) ? ScrollIcon.PREV : ScrollIcon.NEXT; + } + return mouseY < this.y + (this.height / 2) ? ScrollIcon.PREV : ScrollIcon.NEXT; + } + + 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) { } @@ -915,4 +1150,9 @@ public final class BagTabOverlay { CONFIG, LOCK } + + private enum ScrollIcon { + PREV, + NEXT + } } diff --git a/src/main/resources/assets/bagtabs/lang/en_us.json b/src/main/resources/assets/bagtabs/lang/en_us.json index ed14240..4b97a34 100644 --- a/src/main/resources/assets/bagtabs/lang/en_us.json +++ b/src/main/resources/assets/bagtabs/lang/en_us.json @@ -19,6 +19,8 @@ "bagtabs.dock.open": "Open dock settings", "bagtabs.dock.lock": "Lock tab interactions", "bagtabs.dock.unlock": "Unlock tab interactions", + "bagtabs.dock.scroll_prev": "Show previous tabs", + "bagtabs.dock.scroll_next": "Show more tabs", "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", diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_next.png b/src/main/resources/assets/bagtabs/textures/gui/dock_next.png new file mode 100644 index 0000000000000000000000000000000000000000..950a4bd14eee6cb12621af5e0e5e6c1ba72754db GIT binary patch literal 491 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCiji0(?STfpl6L!_1isXU;I(z02_bzw!V7Gl7Dy*Q(A3QXC~ge!>4CfZ<;A z-bkPrXMsm#F#`kNArNL1)$nQn3QCr^MwA5SrKPh# zafqD*D%z16;hE;?sl~tnIesvJDxT7zBZI6cA^& zvw+2OfNT)>RL02g0_a;9jczLg15k1TI|B<)t$~rT0pkLQxga}P7eLIK24sT(6VPNP zu*x7y3m^-s%h138BpdTK@fYWdRl9-AcuyC{5DWjeXK(X1IB>WG#_hgcf1It?!|ejo zmpUW$#%ta!>~E4ARxD(kkuB`d;w|hDdPHy*L)t29rpXP@Ph3r9n6tBzW!bGJ!)J5e raz`-Uxz2OSw7KZ)f*9e9-v=+U?H7~OmP}_@0kYQ9)z4*}Q$iB}n23GV literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/bagtabs/textures/gui/dock_prev.png b/src/main/resources/assets/bagtabs/textures/gui/dock_prev.png new file mode 100644 index 0000000000000000000000000000000000000000..205c966c5aa01f787e70d024b53f5b00575c3772 GIT binary patch literal 496 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCiji0(?STfpl6L!_1isXU;I(z02_b|IGjYje&w%3j*!|DUOmLzu^B6z;Lg5 zZzNERv%n*=n1O-s5C}7hYIrpO1tm*dBT9nv(@M${i&7Z^5;OBk^!!{y6ioFD^$ZQW zIK<8Y741lk@J#dc)MDTOa#$Ip7+Dz@fh;c|mWHxH-q2uV28%NR*@lcv41z#93Wzh? zS-|2sKsE?`Dr01L0rV}5Mz@uL0Vp|voq+|Y*1*WvfN=rDT#%it3m|4q1F}JY31~7C zSY?o<1&{^RWoTdkl8t$r_>1$!s@*_lil>WXh=u>#vyOZX3Op_s7Y2X$XKvQuyJSlh z!(7pa_Dp@#561tfm1YoAVpuRmq$kQH$zW9nha|&lm6fj;8&(G%mw8djHk