Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/main/java/mezz/jei/render/BookmarkListBatchRenderer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package mezz.jei.render;

import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
Expand Down Expand Up @@ -188,7 +190,7 @@ public void setCollapsed(int startIndex, List<IIngredientListElement> collapsedL
// Flatten: BookmarkItem<CollapsedGroupIngredient> → sub-elements (expanded) or single slot (collapsed).
// itemToGroupIndex preserves the bookmark's group index for sub-elements that don't carry it themselves.
List<IIngredientListElement> displayItems = new ArrayList<>();
Map<IIngredientListElement, CollapsedGroupIngredient> itemToCollapsed = new HashMap<>();
Int2ObjectMap<CollapsedGroupIngredient> displayItemGroups = new Int2ObjectOpenHashMap<>();
Map<IIngredientListElement, Integer> itemToGroupIndex = new HashMap<>();

for (IIngredientListElement element : collapsedList) {
Expand All @@ -199,8 +201,8 @@ public void setCollapsed(int startIndex, List<IIngredientListElement> collapsedL
int groupIndex = element.getGroupIndex();
if (isBookmarkItemExpanded(bookmarkItem)) {
for (IIngredientListElement<?> subElement : collapsed.getIngredients()) {
displayItemGroups.put(displayItems.size(), collapsed);
displayItems.add(subElement);
itemToCollapsed.put(subElement, collapsed);
itemToGroupIndex.put(subElement, groupIndex);
expandedElementToBookmark.put(subElement, bookmarkItem);
}
Expand Down Expand Up @@ -270,10 +272,10 @@ public void setCollapsed(int startIndex, List<IIngredientListElement> collapsedL
collapsedRendererToBookmark.put(renderer, bookmarkItem);
} else {
set(ingredientListSlot, displayItem);
CollapsedGroupIngredient parentCollapsed = itemToCollapsed.get(displayItem);
CollapsedGroupIngredient parentCollapsed = displayItemGroups.get(i);
if (parentCollapsed != null) {
collapsedStackIndexed.put(slotIndex, parentCollapsed);
expandedElementToGroup.put(displayItem, parentCollapsed);
expandedElementToGroup.put(ingredientListSlot, parentCollapsed);
expandedGroupSlots.computeIfAbsent(parentCollapsed, k -> new ArrayList<>())
.add(new Rectangle(ingredientListSlot.getArea()));
}
Expand Down
70 changes: 21 additions & 49 deletions src/main/java/mezz/jei/render/CollapsedGroupRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import mezz.jei.ingredients.group.CollapsedGroupIngredient;
import mezz.jei.input.ClickedIngredient;
import mezz.jei.util.CollapsedClickAction;
import mezz.jei.util.CountUtil;
import mezz.jei.util.Translator;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
Expand All @@ -25,9 +26,8 @@
import java.util.List;

/**
* Renders a collapsed group as a single ingredient list slot.
* Shows the first item with a count badge indicating total group size,
* plus a semi-transparent background to distinguish it from normal items.
* Renders a collapsed group showing the first two items scaled as a preview, background tint,
* and a count badge indicating number of items in the group.
*/
public class CollapsedGroupRenderer implements IIngredientRenderer<CollapsedGroupIngredient> {
/** Singleton registered with the ingredient type system — {@code collapsedStack} is null. */
Expand Down Expand Up @@ -68,11 +68,6 @@ public void render(Minecraft minecraft) {
/**
* Stateless render at an arbitrary position — shared by the instance render and
* the {@link IIngredientRenderer} contract.
* For groups with 2+ items, mimics REI's stacked-card icon: each item is rendered at
* 0.75× scale (12 px), offset 4 px so both stay entirely within the 16×16 slot area.
* Back item (upper-right): screen origin (x+4, y+0), occupies (x+4..x+16, y..y+12)
* Front item (lower-left) : screen origin (x+0, y+4), occupies (x..x+12, y+4..y+16)
* Count badge is drawn at 0.75× scale in orange in the bottom-right corner.
*/
private static void renderAt(Minecraft minecraft, CollapsedGroupIngredient ingredient, int x, int y) {
List<IIngredientListElement<?>> ingredients = ingredient.getDisplayIngredients();
Expand All @@ -88,56 +83,39 @@ private static void renderAt(Minecraft minecraft, CollapsedGroupIngredient ingre
GlStateManager.SourceFactor.ONE,
GlStateManager.DestFactor.ZERO
);
// Draw background tint to visually distinguish collapsed groups
// Background tint
GuiScreen.drawRect(x, y, x + 16, y + 16, ingredient.getBackgroundColor());
GlStateManager.disableBlend();

if (ingredients.size() == 1) {
// Single item: render at full size
renderElementAt(minecraft, ingredients.get(0), x, y, 1.0f);
} else {
// 0.75 scale → 12 px icon.
// Back (upper-right): origin at (x+4, y+0) → occupies x+4..x+16, y..y+12
// Front (lower-left) : origin at (x+0, y+4) → occupies x..x+12, y+4..y+16
// Scaled item previews
RenderItem renderItem = minecraft.getRenderItem();
renderElementAt(minecraft, ingredients.get(1), x + 4, y + 0, 0.75f); // back
// Elevate zLevel so the front item's depth values are naturally in front of the
// back item's geometry. Using GL_LEQUAL (normal) keeps the front item's own
// internal face culling intact — GL_ALWAYS would break tile-entity models.
// Back
renderElementAt(minecraft, ingredients.get(1), x + 4, y + 0, 0.75f);
float prevZLevel = renderItem.zLevel;
renderItem.zLevel += 100;
renderElementAt(minecraft, ingredients.get(0), x + 0, y + 4, 0.75f); // front
// Front
renderElementAt(minecraft, ingredients.get(0), x + 0, y + 4, 0.75f);
renderItem.zLevel = prevZLevel;
}

// Count badge: 0.75× scale, orange, right-aligned at the bottom of the slot
// Count badge
int count = ingredient.size();
if (count > 1) {
String countStr = CountUtil.minifyCountString(count);
float badgeScale = count <= 999 ? 0.75f : 0.5f;
FontRenderer fontRenderer = minecraft.fontRenderer;
String countStr = String.valueOf(count);
GlStateManager.disableLighting();
GlStateManager.disableDepth();
GlStateManager.disableBlend();
final float badgeScale = 0.75f;
// Convert desired screen position to scaled-coordinate space.
// Screen right edge: x+16 → scaled coord (x+16)/badgeScale
// Screen top of text: y+10 → scaled coord (y+10)/badgeScale
int textWidth = fontRenderer.getStringWidth(countStr);
int scaledRight = (int) ((x + 16) / badgeScale);
int scaledTop = (int) ((y + 10) / badgeScale);
GlStateManager.pushMatrix();
GlStateManager.scale(badgeScale, badgeScale, 1.0f);
fontRenderer.drawStringWithShadow(countStr, scaledRight - textWidth, scaledTop, 0xFFAA00);
GlStateManager.popMatrix();
GlStateManager.enableDepth();

CountUtil.renderStringAsCount(fontRenderer, countStr, x, y, 0xFFAA00, true, badgeScale);
}

drawCollapsedBorder(x, y, ingredient.getBorderColor());
}

/**
* Renders one ingredient at (x, y) at the given scale using the GL matrix stack.
* Delegates to renderItemAndEffectIntoGUI so all item types (2D, 3D, built-in) render correctly.
*/
private static void renderElementAt(Minecraft minecraft, IIngredientListElement<?> element, int x, int y, float scale) {
Object ingredient = element.getIngredient();
Expand Down Expand Up @@ -166,7 +144,7 @@ private static void drawCollapsedBorder(int x, int y, int borderColor) {
GlStateManager.SourceFactor.ONE,
GlStateManager.DestFactor.ZERO
);
// Small triangle indicator in the top-left corner to show it's collapsible
// top left indicator
GlStateManager.disableLighting();
GlStateManager.disableDepth();
GuiScreen.drawRect(x, y, x + 4, y + 1, borderColor);
Expand All @@ -175,9 +153,6 @@ private static void drawCollapsedBorder(int x, int y, int borderColor) {
GlStateManager.disableBlend();
}

// --- IIngredientRenderer<CollapsedStack> implementation ---
// INSTANCE (null stack) is registered with the ingredient type system.

@Override
public void render(Minecraft minecraft, int xPosition, int yPosition, @Nullable CollapsedGroupIngredient ingredient) {
if (ingredient == null || ingredient.isEmpty()) {
Expand Down Expand Up @@ -207,16 +182,16 @@ public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) {
List<IIngredientListElement<?>> ingredients = collapsedStack.getDisplayIngredients();
if (ingredients.isEmpty()) return;

// Single-item group (e.g. search filtered to one result): show the item's native tooltip
// Single-item group - show the item's native tooltip
if (ingredients.size() == 1) {
new IngredientRenderer<>(ingredients.get(0)).drawTooltip(minecraft, mouseX, mouseY);
return;
}

FontRenderer font = minecraft.fontRenderer;
final int COLS = 8;
final int SLOT = 18; // 16px icon + 1px padding each side
final int MAX_VISIBLE = COLS * 2 + 7; // 23 = rows of 8, 8, 7
final int SLOT = 18;
final int MAX_VISIBLE = COLS * 2 + 7;

int total = ingredients.size();
int shown = Math.min(total, MAX_VISIBLE);
Expand All @@ -228,8 +203,7 @@ public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) {

String header = TextFormatting.GOLD + collapsedStack.getDisplayName()
+ TextFormatting.GRAY + " (" + total + " items)";
// In OPEN_GROUP mode, alt+click uses first item; show that as the hint.
// In FIRST_ITEM mode, alt+click expands; show that instead.
// OPEN_GROUP/FIRST_ITEM hint tooltip
String hint = TextFormatting.YELLOW + Translator.translateToLocal(
Config.getCollapsedClickAction() == CollapsedClickAction.OPEN_GROUP
? "hei.tooltip.collapsed.expand.firstItem"
Expand All @@ -250,7 +224,7 @@ public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) {
GlStateManager.disableLighting();
GlStateManager.disableDepth();

// Draw tooltip background (MC-style dark purple box with gradient border)
// Tooltip background
final int z = 300;
int bg = 0xF0100010, bs = 0x505000FF, be = (bs & 0xFEFEFE) >> 1 | (bs & 0xFF000000);
GuiUtils.drawGradientRect(z, tx-3, ty-4, tx+tw+3, ty-3, bg, bg);
Expand Down Expand Up @@ -288,7 +262,7 @@ public void drawTooltip(Minecraft minecraft, int mouseX, int mouseY) {
RenderHelper.disableStandardItemLighting();
GlStateManager.popMatrix();

// "+N" overflow indicator in 8th slot of row 3 (only when there are hidden items)
// "+N" overflow indicator
GlStateManager.disableDepth();
GlStateManager.disableLighting();
if (overflow > 0) {
Expand All @@ -315,15 +289,13 @@ public ClickedIngredient<?> getClickedIngredient() {
if (ingredients.isEmpty()) {
return null;
}
// Return CollapsedStack directly — it is a registered IIngredientType
return ClickedIngredient.create(collapsedStack, area);
}

public boolean isMouseOver(int mouseX, int mouseY) {
return area.contains(mouseX, mouseY);
}

@SuppressWarnings("unchecked")
private static <T> void renderIngredient(Minecraft minecraft, int x, int y, IIngredientListElement<T> element) {
IIngredientRenderer<T> renderer = element.getIngredientRenderer();
T ingredient = element.getIngredient();
Expand Down
24 changes: 14 additions & 10 deletions src/main/java/mezz/jei/render/IngredientListBatchRenderer.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mezz.jei.render;

import com.google.common.base.Preconditions;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import mezz.jei.api.ingredients.ISlowRenderItem;
import mezz.jei.config.Config;
Expand Down Expand Up @@ -38,7 +40,7 @@ public class IngredientListBatchRenderer {
protected final List<IngredientRenderer> renderOther = new ArrayList<>();
protected final List<CollapsedGroupRenderer> renderCollapsed = new ArrayList<>();
protected final Map<Integer, CollapsedGroupIngredient> collapsedStackIndexed = new HashMap<>();
protected final Map<IIngredientListElement<?>, CollapsedGroupIngredient> expandedElementToGroup = new HashMap<>();
protected final Map<IngredientListSlot, CollapsedGroupIngredient> expandedElementToGroup = new HashMap<>();
// Per-group list of individual slot rectangles (used for per-slot fill + edge-detection border).
protected final Map<CollapsedGroupIngredient, List<Rectangle>> expandedGroupSlots = new HashMap<>();

Expand Down Expand Up @@ -159,7 +161,7 @@ public void setCollapsed(final int startIndex, List<IIngredientListElement> coll
// This ensures expanded groups don't break pagination — firstItemIndex is an index into
// the flattened view, which matches what collapsedSize() now returns.
List<IIngredientListElement> displayItems = new ArrayList<>();
Map<IIngredientListElement, CollapsedGroupIngredient> itemToCollapsed = new HashMap<>();
Int2ObjectMap<CollapsedGroupIngredient> displayItemGroups = new Int2ObjectOpenHashMap<>();
for (IIngredientListElement obj : collapsedList) {
if (obj instanceof CollapsedGroupIngredient) {
CollapsedGroupIngredient collapsed = (CollapsedGroupIngredient) obj;
Expand All @@ -171,13 +173,12 @@ public void setCollapsed(final int startIndex, List<IIngredientListElement> coll
} else {
// Expanded: add each ingredient individually, track which belong to this group
for (IIngredientListElement<?> element : filterIngredients) {
displayItemGroups.put(displayItems.size(), collapsed);
displayItems.add(element);
itemToCollapsed.put(element, collapsed);
}
}
} else if (collapsed.size() == 1) {
// Single-item group: render as a plain ingredient slot without collapsed visuals.
// Not tracked in itemToCollapsed so clicks/hover treat it as a normal item.
displayItems.add(collapsed.getDisplayIngredients().get(0));
} else {
// Collapsed: add the CollapsedStack itself as a single display item
Expand Down Expand Up @@ -211,10 +212,10 @@ public void setCollapsed(final int startIndex, List<IIngredientListElement> coll
collapsedStackIndexed.put(slotIndex, collapsed);
} else {
set(ingredientListSlot, displayItem);
CollapsedGroupIngredient parentCollapsed = itemToCollapsed.get(displayItem);
CollapsedGroupIngredient parentCollapsed = displayItemGroups.get(i);
if (parentCollapsed != null) {
collapsedStackIndexed.put(slotIndex, parentCollapsed);
expandedElementToGroup.put(displayItem, parentCollapsed);
expandedElementToGroup.put(ingredientListSlot, parentCollapsed);
expandedGroupSlots.computeIfAbsent(parentCollapsed, k -> new ArrayList<>())
.add(new Rectangle(ingredientListSlot.getArea()));
}
Expand Down Expand Up @@ -336,11 +337,14 @@ public IngredientRenderer getHovered(int mouseX, int mouseY) {

@Nullable
public CollapsedGroupIngredient getExpandedCollapsedGroupAt(int mouseX, int mouseY) {
IngredientRenderer hovered = getHovered(mouseX, mouseY);
if (hovered == null) {
return null;
for (List<IngredientListSlot> row : slots) {
for (IngredientListSlot slot : row) {
if (slot.isMouseOver(mouseX, mouseY)) {
return expandedElementToGroup.get(slot);
}
}
}
return expandedElementToGroup.get(hovered.getElement());
return null;
}

public void renderExpandedGroupOutlines() {
Expand Down
36 changes: 27 additions & 9 deletions src/main/java/mezz/jei/util/CountUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public static void renderCountString(FontRenderer font, long count, int xPositio
}

/**
* Renders string as it would be if it was a count on an itemstack
* Renders string as it would be if it was a count on an itemstack.
*
* @param font The font renderer
* @param count The count
* @param xPosition X coordinate
Expand All @@ -50,23 +51,40 @@ public static void renderCountString(FontRenderer font, long count, int xPositio
* (mimics the way item stack counts are rendered),
* or to use xPosition and yPosition as absolute screen coordinates
* @param scale True to scale down by 1/2 in the screen-space
* @see #renderStringAsCount(FontRenderer, String, int, int, int, boolean, float)
*/
public static void renderStringAsCount(FontRenderer font, String count, int xPosition, int yPosition, int color, boolean relative, boolean scale) {
renderStringAsCount(font, count, xPosition, yPosition, color, relative, scale ? 0.5f : 1.0f);
}

/**
* Renders string as it would be if it was a count on an itemstack
* @param font The font renderer
* @param count The count
* @param xPosition X coordinate
* @param yPosition Y coordinate
* @param color Color of rendered string
* @param relative Whether or not to render relative to the xPosition and yPosition
* (mimics the way item stack counts are rendered),
* or to use xPosition and yPosition as absolute screen coordinates
* @param scale Value to scale the count by
*/
public static void renderStringAsCount(FontRenderer font, String count, int xPosition, int yPosition, int color, boolean relative, float scale) {
GlStateManager.pushMatrix();
GlStateManager.disableLighting();
GlStateManager.disableDepth();
GlStateManager.disableBlend();

if (scale) {
GlStateManager.scale(0.5F, 0.5F, 1.0F);
if (scale != 1.0f) {
GlStateManager.scale(scale, scale, 1.0F);
}

int x = scale ?
(relative ? xPosition + 16 : xPosition) * 2 - font.getStringWidth(count) :
(relative ? xPosition + 17 : xPosition) - font.getStringWidth(count);
int y = scale ?
(relative ? yPosition + 16 : yPosition) * 2 - 8 :
relative ? yPosition + 9 : yPosition;
int x = scale != 1.0f
? (int)((relative ? xPosition + 16 : xPosition) / scale) - font.getStringWidth(count)
: (relative ? xPosition + 17 : xPosition) - font.getStringWidth(count);
int y = scale != 1.0f
? (int)((relative ? yPosition + 16 : yPosition) / scale) - 8
: (relative ? yPosition + 9 : yPosition);

font.drawStringWithShadow(count, x, y, color);

Expand Down