diff --git a/src/main/java/com/lambda/mixin/items/BlockItemMixin.java b/src/main/java/com/lambda/mixin/items/BlockItemMixin.java
new file mode 100644
index 000000000..c599cea55
--- /dev/null
+++ b/src/main/java/com/lambda/mixin/items/BlockItemMixin.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.mixin.items;
+
+import com.lambda.module.modules.render.ContainerPreview;
+import net.minecraft.item.BlockItem;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.tooltip.TooltipData;
+import org.spongepowered.asm.mixin.Mixin;
+
+import java.util.Optional;
+
+@Mixin(BlockItem.class)
+public class BlockItemMixin extends Item {
+ public BlockItemMixin(Settings settings) {
+ super(settings);
+ }
+
+ @Override
+ public Optional getTooltipData(ItemStack stack) {
+ if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isPreviewableContainer(stack)) {
+ return Optional.of(new ContainerPreview.ContainerComponent(stack));
+ }
+ return super.getTooltipData(stack);
+ }
+}
diff --git a/src/main/java/com/lambda/mixin/render/DrawContextMixin.java b/src/main/java/com/lambda/mixin/render/DrawContextMixin.java
new file mode 100644
index 000000000..825e8b9a0
--- /dev/null
+++ b/src/main/java/com/lambda/mixin/render/DrawContextMixin.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.mixin.render;
+
+import com.lambda.module.modules.render.ContainerPreview;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.client.gui.tooltip.TooltipPositioner;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.tooltip.TooltipData;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+import java.util.Optional;
+
+@Mixin(DrawContext.class)
+public class DrawContextMixin {
+ @Inject(method = "drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;IILnet/minecraft/util/Identifier;)V", at = @At("HEAD"), cancellable = true)
+ private void onDrawTooltip(TextRenderer textRenderer, List text, Optional data, int x, int y, @Nullable Identifier texture, CallbackInfo ci) {
+ if (!ContainerPreview.INSTANCE.isEnabled()) return;
+
+ if (ContainerPreview.isRenderingSubTooltip()) return;
+
+ if (ContainerPreview.isLocked()) {
+ ci.cancel();
+ ContainerPreview.renderLockedTooltip((DrawContext)(Object)this, textRenderer);
+ return;
+ }
+
+ if (data.isPresent() && data.get() instanceof ContainerPreview.ContainerComponent component) {
+ ci.cancel();
+ ContainerPreview.renderShulkerTooltip((DrawContext)(Object)this, textRenderer, component, x, y);
+ }
+ }
+}
diff --git a/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java b/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java
new file mode 100644
index 000000000..193cdc516
--- /dev/null
+++ b/src/main/java/com/lambda/mixin/render/HandledScreenMixin.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.mixin.render;
+
+import com.lambda.module.modules.render.ContainerPreview;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(HandledScreen.class)
+public class HandledScreenMixin {
+ @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
+ private void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) {
+ if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) {
+ if (ContainerPreview.isMouseOverLockedTooltip((int) mouseX, (int) mouseY)) {
+ cir.setReturnValue(true);
+ }
+ }
+ }
+
+ @Inject(method = "mouseReleased", at = @At("HEAD"), cancellable = true)
+ private void onMouseReleased(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) {
+ if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) {
+ if (ContainerPreview.isMouseOverLockedTooltip((int) mouseX, (int) mouseY)) {
+ cir.setReturnValue(true);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/lambda/mixin/render/ScreenMixin.java b/src/main/java/com/lambda/mixin/render/ScreenMixin.java
index 1780bb8a7..9c4972653 100644
--- a/src/main/java/com/lambda/mixin/render/ScreenMixin.java
+++ b/src/main/java/com/lambda/mixin/render/ScreenMixin.java
@@ -18,7 +18,11 @@
package com.lambda.mixin.render;
import com.lambda.gui.components.QuickSearch;
+import com.lambda.module.modules.render.ContainerPreview;
import com.lambda.module.modules.render.NoRender;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
import org.lwjgl.glfw.GLFW;
@@ -42,4 +46,13 @@ private void onKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfo
private void injectRenderInGameBackground(DrawContext context, CallbackInfo ci) {
if (NoRender.INSTANCE.isEnabled() && NoRender.getNoGuiShadow()) ci.cancel();
}
+
+ @WrapOperation(method = "renderWithTooltip", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/gui/DrawContext;IIF)V"))
+ private void wrapRender(Screen instance, DrawContext context, int mouseX, int mouseY, float deltaTicks, Operation original) {
+ original.call(instance, context, mouseX, mouseY, deltaTicks);
+
+ if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) {
+ ContainerPreview.renderLockedTooltip(context, MinecraftClient.getInstance().textRenderer);
+ }
+ }
}
diff --git a/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java b/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java
index b33fba5c1..1088f5c65 100644
--- a/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java
+++ b/src/main/java/com/lambda/mixin/render/TooltipComponentMixin.java
@@ -17,6 +17,7 @@
package com.lambda.mixin.render;
+import com.lambda.module.modules.render.ContainerPreview;
import com.lambda.module.modules.render.MapPreview;
import net.minecraft.client.gui.tooltip.BundleTooltipComponent;
import net.minecraft.client.gui.tooltip.ProfilesTooltipComponent;
@@ -32,6 +33,11 @@
public interface TooltipComponentMixin {
@Inject(method = "of(Lnet/minecraft/item/tooltip/TooltipData;)Lnet/minecraft/client/gui/tooltip/TooltipComponent;", at = @At("HEAD"), cancellable = true)
private static void of(TooltipData tooltipData, CallbackInfoReturnable cir) {
+ if (ContainerPreview.INSTANCE.isEnabled() && tooltipData instanceof ContainerPreview.ContainerComponent containerComponent) {
+ cir.setReturnValue(containerComponent);
+ return;
+ }
+
if (MapPreview.INSTANCE.isEnabled()) cir.setReturnValue((switch (tooltipData) {
case MapPreview.MapComponent mapComponent -> mapComponent;
case BundleTooltipData bundleTooltipData -> new BundleTooltipComponent(bundleTooltipData.contents());
diff --git a/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt
new file mode 100644
index 000000000..04674e1cb
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/render/ContainerPreview.kt
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.Lambda.mc
+import com.lambda.config.settings.complex.Bind
+import com.lambda.interaction.material.container.containers.EnderChestContainer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.KeyCode
+import com.lambda.util.item.ItemStackUtils.shulkerBoxContents
+import com.lambda.util.item.ItemUtils.shulkerBoxes
+import net.minecraft.block.ShulkerBoxBlock
+import net.minecraft.client.font.TextRenderer
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.gui.tooltip.TooltipComponent
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.item.BlockItem
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.item.tooltip.TooltipData
+import net.minecraft.util.DyeColor
+import net.minecraft.util.Identifier
+import org.lwjgl.glfw.GLFW
+
+object ContainerPreview : Module(
+ name = "ContainerPreview",
+ description = "Renders shulker box contents visually in tooltips",
+ tag = ModuleTag.RENDER,
+) {
+ private val lockKey by setting("Lock Key", Bind(KeyCode.LeftShift.code, 0, -1), "Key to lock the tooltip in place for item interaction")
+ private val useShift by setting("Use Shift", true, "Use shift key to lock tooltip (overrides lock key)")
+ private val colorTint by setting("Color Tint", true, "Tint the background with the shulker box color")
+
+ private val background = Identifier.ofVanilla("textures/gui/container/shulker_box.png")
+
+ private var lockedStack: ItemStack? = null
+ private var lockedX: Int = 0
+ private var lockedY: Int = 0
+
+ @JvmStatic
+ var isRenderingSubTooltip: Boolean = false
+ private set
+
+ private const val ROWS = 3
+ private const val COLS = 9
+ private const val SLOT_SIZE = 18
+ private const val PADDING = 7
+ private const val TITLE_HEIGHT = 14
+
+ @JvmStatic
+ val isLocked: Boolean
+ get() = lockedStack != null
+
+ @JvmStatic
+ fun isLockKeyPressed(): Boolean {
+ if (!isEnabled) return false
+ if (useShift) return Screen.hasShiftDown()
+ val handle = mc.window.handle
+ return GLFW.glfwGetKey(handle, lockKey.key) == GLFW.GLFW_PRESS
+ }
+
+ private fun getTooltipWidth() = PADDING + COLS * SLOT_SIZE + PADDING
+ private fun getTooltipHeight() = TITLE_HEIGHT + ROWS * SLOT_SIZE + PADDING
+
+ /**
+ * Check if mouse is over the locked tooltip area (for click blocking)
+ */
+ @JvmStatic
+ fun isMouseOverLockedTooltip(mouseX: Int, mouseY: Int): Boolean {
+ if (!isLocked) return false
+ val width = getTooltipWidth()
+ val height = getTooltipHeight()
+ return mouseX >= lockedX && mouseX < lockedX + width &&
+ mouseY >= lockedY && mouseY < lockedY + height
+ }
+
+ /**
+ * Calculate text color based on background luminance.
+ * Returns dark text for light backgrounds and white text for dark backgrounds.
+ */
+ private fun getTextColor(tintColor: Int): Int {
+ val r = ((tintColor shr 16) and 0xFF) / 255f
+ val g = ((tintColor shr 8) and 0xFF) / 255f
+ val b = (tintColor and 0xFF) / 255f
+ val luminance = 0.299f * r + 0.587f * g + 0.114f * b
+ return if (luminance > 0.5f) 0x404040 else 0xFFFFFF
+ }
+
+ @JvmStatic
+ fun renderShulkerTooltip(
+ context: DrawContext,
+ textRenderer: TextRenderer,
+ component: ContainerComponent,
+ mouseX: Int,
+ mouseY: Int
+ ) {
+ val width = getTooltipWidth()
+ val height = getTooltipHeight()
+
+ val lockKeyPressed = isLockKeyPressed()
+
+ if (lockKeyPressed && lockedStack == null) {
+ lockedStack = component.stack.copy()
+ lockedX = calculateTooltipX(mouseX, width)
+ lockedY = calculateTooltipY(mouseY, height)
+ } else if (!lockKeyPressed && lockedStack != null) {
+ lockedStack = null
+ }
+
+ if (isLocked) {
+ renderLockedTooltipInternal(context, textRenderer)
+ return
+ }
+
+ renderTooltipForStack(context, textRenderer, component.stack, calculateTooltipX(mouseX, width), calculateTooltipY(mouseY, height), false)
+ }
+
+ /**
+ * Render the locked tooltip - called from mixin when we're locked
+ */
+ @JvmStatic
+ fun renderLockedTooltip(context: DrawContext, textRenderer: TextRenderer) {
+ if (!isLockKeyPressed()) {
+ lockedStack = null
+ return
+ }
+ renderLockedTooltipInternal(context, textRenderer)
+ }
+
+ private fun renderLockedTooltipInternal(context: DrawContext, textRenderer: TextRenderer) {
+ val stack = lockedStack ?: return
+ renderTooltipForStack(context, textRenderer, stack, lockedX, lockedY, true)
+ }
+
+ private fun renderTooltipForStack(context: DrawContext, textRenderer: TextRenderer, stack: ItemStack, x: Int, y: Int, allowHover: Boolean) {
+ val contents = getContainerContents(stack)
+ val width = getTooltipWidth()
+ val height = getTooltipHeight()
+
+ val matrices = context.matrices
+ matrices.push()
+ matrices.translate(0f, 0f, 400f)
+
+ val tintColor = getContainerTintColor(stack)
+
+ drawBackground(context, x, y, width, height, tintColor)
+ val name = stack.name
+ val textColor = getTextColor(tintColor)
+ context.drawText(textRenderer, name, x + PADDING, y + 4, textColor, false)
+
+ val slotsStartX = x + PADDING
+ val slotsStartY = y + TITLE_HEIGHT
+
+ val actualMouseX = (mc.mouse.x * mc.window.scaledWidth / mc.window.width).toInt()
+ val actualMouseY = (mc.mouse.y * mc.window.scaledHeight / mc.window.height).toInt()
+
+ var hoveredStack: ItemStack? = null
+ var hoveredSlotX = 0
+ var hoveredSlotY = 0
+
+ for ((index, item) in contents.withIndex()) {
+ if (index >= COLS * ROWS) break
+
+ val slotCol = index % COLS
+ val slotRow = index / COLS
+
+ val slotX = slotsStartX + slotCol * SLOT_SIZE
+ val slotY = slotsStartY + slotRow * SLOT_SIZE
+ val itemX = slotX + 1
+ val itemY = slotY + 1
+
+ if (allowHover) {
+ val isHovered = actualMouseX >= slotX && actualMouseX < slotX + SLOT_SIZE &&
+ actualMouseY >= slotY && actualMouseY < slotY + SLOT_SIZE
+
+ if (isHovered && !item.isEmpty) {
+ context.fill(itemX, itemY, itemX + 16, itemY + 16, 0x80FFFFFF.toInt())
+ hoveredStack = item
+ hoveredSlotX = actualMouseX
+ hoveredSlotY = actualMouseY
+ }
+ }
+
+ if (!item.isEmpty) {
+ context.drawItem(item, itemX, itemY)
+ context.drawStackOverlay(textRenderer, item, itemX, itemY)
+ }
+ }
+
+ matrices.pop()
+
+ hoveredStack?.let { stack ->
+ matrices.push()
+ matrices.translate(0f, 0f, 500f)
+
+ if (isPreviewableContainer(stack)) {
+ val nestedWidth = getTooltipWidth()
+ val nestedHeight = getTooltipHeight()
+ val nestedX = calculateTooltipX(hoveredSlotX, nestedWidth)
+ val nestedY = calculateTooltipY(hoveredSlotY, nestedHeight)
+ renderTooltipForStack(context, textRenderer, stack, nestedX, nestedY, false)
+ } else {
+ isRenderingSubTooltip = true
+ try {
+ context.drawItemTooltip(textRenderer, stack, hoveredSlotX, hoveredSlotY)
+ } finally {
+ isRenderingSubTooltip = false
+ }
+ }
+ matrices.pop()
+ }
+ }
+
+ private fun getContainerContents(stack: ItemStack): List {
+ return when {
+ isShulkerBox(stack) -> stack.shulkerBoxContents
+ isEnderChest(stack) -> EnderChestContainer.stacks
+ else -> emptyList()
+ }
+ }
+
+ private fun getContainerTintColor(stack: ItemStack): Int {
+ if (!colorTint) return 0xFFFFFFFF.toInt()
+
+ return when {
+ isShulkerBox(stack) -> {
+ val color = getShulkerColor(stack)
+ color?.entityColor ?: 0xFFFFFFFF.toInt()
+ }
+ isEnderChest(stack) -> 0xFF1E1E2E.toInt()
+ else -> 0xFFFFFFFF.toInt()
+ }
+ }
+
+ private fun calculateTooltipX(mouseX: Int, width: Int): Int {
+ val screenWidth = mc.window.scaledWidth
+ var x = mouseX + 12
+ if (x + width > screenWidth) {
+ x = mouseX - width - 12
+ }
+ if (x < 0) x = 0
+ return x
+ }
+
+ private fun calculateTooltipY(mouseY: Int, height: Int): Int {
+ val screenHeight = mc.window.scaledHeight
+ var y = mouseY - 12
+ if (y + height > screenHeight) {
+ y = screenHeight - height
+ }
+ if (y < 0) y = 0
+ return y
+ }
+
+ private fun drawBackground(context: DrawContext, x: Int, y: Int, width: Int, height: Int, tintColor: Int) {
+ // Draw the shulker box texture background with tint
+ // The shulker_box.png texture is 176x166
+ // Top part (title area): y=0 to y=17
+ // Slot area: y=17 to y=89 (3 rows of 18px each + borders)
+ // Bottom: y=160 onwards
+
+ context.drawTexture(
+ RenderLayer::getGuiTextured,
+ background,
+ x, y,
+ 0f, 0f,
+ width, TITLE_HEIGHT,
+ 256, 256,
+ tintColor
+ )
+
+ // Middle rows
+ (0 until ROWS).forEach { row ->
+ context.drawTexture(
+ RenderLayer::getGuiTextured,
+ background,
+ x, y + TITLE_HEIGHT + row * SLOT_SIZE,
+ 0f, 17f,
+ width, SLOT_SIZE,
+ 256, 256,
+ tintColor
+ )
+ }
+
+ // Bottom
+ context.drawTexture(
+ RenderLayer::getGuiTextured,
+ background,
+ x, y + TITLE_HEIGHT + ROWS * SLOT_SIZE,
+ 0f, 160f,
+ width, PADDING,
+ 256, 256,
+ tintColor
+ )
+ }
+
+ private fun getShulkerColor(stack: ItemStack): DyeColor? {
+ val item = stack.item
+ if (item is BlockItem) {
+ val block = item.block
+ if (block is ShulkerBoxBlock) {
+ return block.color
+ }
+ }
+ return null
+ }
+
+ @JvmStatic
+ fun isShulkerBox(stack: ItemStack) = stack.item in shulkerBoxes
+
+ @JvmStatic
+ fun isEnderChest(stack: ItemStack) = stack.item == Items.ENDER_CHEST && EnderChestContainer.stacks.isNotEmpty()
+
+ @JvmStatic
+ fun isPreviewableContainer(stack: ItemStack) = isShulkerBox(stack) || isEnderChest(stack)
+
+ open class ContainerComponent(val stack: ItemStack) : TooltipData, TooltipComponent {
+ override fun drawItems(textRenderer: TextRenderer, x: Int, y: Int, width: Int, height: Int, context: DrawContext) {}
+ override fun getHeight(textRenderer: TextRenderer): Int = 0
+ override fun getWidth(textRenderer: TextRenderer): Int = 0
+ }
+}
diff --git a/src/main/resources/lambda.mixins.common.json b/src/main/resources/lambda.mixins.common.json
index c865de961..f11bdfb07 100644
--- a/src/main/resources/lambda.mixins.common.json
+++ b/src/main/resources/lambda.mixins.common.json
@@ -21,6 +21,7 @@
"input.KeyboardMixin",
"input.MouseMixin",
"items.BarrierBlockMixin",
+ "items.BlockItemMixin",
"items.FilledMapItemMixin",
"network.ClientConnectionMixin",
"network.ClientLoginNetworkMixin",
@@ -46,11 +47,13 @@
"render.ChunkOcclusionDataBuilderMixin",
"render.DebugHudMixin",
"render.DebugRendererMixin",
+ "render.DrawContextMixin",
"render.ElytraFeatureRendererMixin",
"render.EnchantingTableBlockEntityRendererMixin",
"render.EntityRendererMixin",
"render.FluidRendererMixin",
"render.GameRendererMixin",
+ "render.HandledScreenMixin",
"render.GlStateManagerMixin",
"render.HeadFeatureRendererMixin",
"render.HeldItemRendererMixin",