package net.minecraft.client.gui.components; import com.google.common.collect.Lists; import java.util.AbstractList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Predicate; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.events.ContainerEventHandler; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; import net.minecraft.client.gui.narration.NarratableEntry; import net.minecraft.client.gui.narration.NarratedElementType; import net.minecraft.client.gui.narration.NarrationElementOutput; import net.minecraft.client.gui.navigation.ScreenDirection; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.renderer.RenderType; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; @Environment(EnvType.CLIENT) public abstract class AbstractSelectionList> extends AbstractContainerWidget { private static final ResourceLocation MENU_LIST_BACKGROUND = ResourceLocation.withDefaultNamespace("textures/gui/menu_list_background.png"); private static final ResourceLocation INWORLD_MENU_LIST_BACKGROUND = ResourceLocation.withDefaultNamespace("textures/gui/inworld_menu_list_background.png"); protected final Minecraft minecraft; protected final int itemHeight; private final List children = new AbstractSelectionList.TrackedList(); protected boolean centerListVertically = true; private boolean renderHeader; protected int headerHeight; @Nullable private E selected; @Nullable private E hovered; public AbstractSelectionList(Minecraft minecraft, int width, int height, int y, int itemHeight) { super(0, y, width, height, CommonComponents.EMPTY); this.minecraft = minecraft; this.itemHeight = itemHeight; } public AbstractSelectionList(Minecraft minecraft, int width, int height, int y, int itemHeight, int headerHeight) { this(minecraft, width, height, y, itemHeight); this.renderHeader = true; this.headerHeight = headerHeight; } @Nullable public E getSelected() { return this.selected; } public void setSelectedIndex(int selected) { if (selected == -1) { this.setSelected(null); } else if (this.getItemCount() != 0) { this.setSelected(this.getEntry(selected)); } } public void setSelected(@Nullable E selected) { this.selected = selected; } public E getFirstElement() { return (E)this.children.get(0); } @Nullable public E getFocused() { return (E)super.getFocused(); } @Override public final List children() { return this.children; } protected void clearEntries() { this.children.clear(); this.selected = null; } public void replaceEntries(Collection entries) { this.clearEntries(); this.children.addAll(entries); } protected E getEntry(int index) { return (E)this.children().get(index); } protected int addEntry(E entry) { this.children.add(entry); return this.children.size() - 1; } protected void addEntryToTop(E entry) { double d = this.maxScrollAmount() - this.scrollAmount(); this.children.add(0, entry); this.setScrollAmount(this.maxScrollAmount() - d); } protected boolean removeEntryFromTop(E entry) { double d = this.maxScrollAmount() - this.scrollAmount(); boolean bl = this.removeEntry(entry); this.setScrollAmount(this.maxScrollAmount() - d); return bl; } protected int getItemCount() { return this.children().size(); } protected boolean isSelectedItem(int index) { return Objects.equals(this.getSelected(), this.children().get(index)); } @Nullable protected final E getEntryAtPosition(double mouseX, double mouseY) { int i = this.getRowWidth() / 2; int j = this.getX() + this.width / 2; int k = j - i; int l = j + i; int m = Mth.floor(mouseY - this.getY()) - this.headerHeight + (int)this.scrollAmount() - 4; int n = m / this.itemHeight; return (E)(mouseX >= k && mouseX <= l && n >= 0 && m >= 0 && n < this.getItemCount() ? this.children().get(n) : null); } public void updateSize(int width, HeaderAndFooterLayout layout) { this.updateSizeAndPosition(width, layout.getContentHeight(), layout.getHeaderHeight()); } public void updateSizeAndPosition(int width, int height, int y) { this.setSize(width, height); this.setPosition(0, y); this.refreshScrollAmount(); } @Override protected int contentHeight() { return this.getItemCount() * this.itemHeight + this.headerHeight + 4; } protected void renderHeader(GuiGraphics guiGraphics, int x, int y) { } protected void renderDecorations(GuiGraphics guiGraphics, int mouseX, int mouseY) { } @Override public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { this.hovered = this.isMouseOver(mouseX, mouseY) ? this.getEntryAtPosition(mouseX, mouseY) : null; this.renderListBackground(guiGraphics); this.enableScissor(guiGraphics); if (this.renderHeader) { int i = this.getRowLeft(); int j = this.getY() + 4 - (int)this.scrollAmount(); this.renderHeader(guiGraphics, i, j); } this.renderListItems(guiGraphics, mouseX, mouseY, partialTick); guiGraphics.disableScissor(); this.renderListSeparators(guiGraphics); this.renderScrollbar(guiGraphics); this.renderDecorations(guiGraphics, mouseX, mouseY); } protected void renderListSeparators(GuiGraphics guiGraphics) { ResourceLocation resourceLocation = this.minecraft.level == null ? Screen.HEADER_SEPARATOR : Screen.INWORLD_HEADER_SEPARATOR; ResourceLocation resourceLocation2 = this.minecraft.level == null ? Screen.FOOTER_SEPARATOR : Screen.INWORLD_FOOTER_SEPARATOR; guiGraphics.blit(RenderType::guiTextured, resourceLocation, this.getX(), this.getY() - 2, 0.0F, 0.0F, this.getWidth(), 2, 32, 2); guiGraphics.blit(RenderType::guiTextured, resourceLocation2, this.getX(), this.getBottom(), 0.0F, 0.0F, this.getWidth(), 2, 32, 2); } protected void renderListBackground(GuiGraphics guiGraphics) { ResourceLocation resourceLocation = this.minecraft.level == null ? MENU_LIST_BACKGROUND : INWORLD_MENU_LIST_BACKGROUND; guiGraphics.blit( RenderType::guiTextured, resourceLocation, this.getX(), this.getY(), this.getRight(), this.getBottom() + (int)this.scrollAmount(), this.getWidth(), this.getHeight(), 32, 32 ); } protected void enableScissor(GuiGraphics guiGraphics) { guiGraphics.enableScissor(this.getX(), this.getY(), this.getRight(), this.getBottom()); } protected void centerScrollOn(E entry) { this.setScrollAmount(this.children().indexOf(entry) * this.itemHeight + this.itemHeight / 2 - this.height / 2); } protected void ensureVisible(E entry) { int i = this.getRowTop(this.children().indexOf(entry)); int j = i - this.getY() - 4 - this.itemHeight; if (j < 0) { this.scroll(j); } int k = this.getBottom() - i - this.itemHeight - this.itemHeight; if (k < 0) { this.scroll(-k); } } private void scroll(int scroll) { this.setScrollAmount(this.scrollAmount() + scroll); } @Override protected double scrollRate() { return this.itemHeight / 2.0; } @Override protected int scrollBarX() { return this.getRowRight() + 6 + 2; } @Override public Optional getChildAt(double mouseX, double mouseY) { return Optional.ofNullable(this.getEntryAtPosition(mouseX, mouseY)); } @Override public void setFocused(@Nullable GuiEventListener focused) { E entry = this.getFocused(); if (entry != focused && entry instanceof ContainerEventHandler containerEventHandler) { containerEventHandler.setFocused(null); } super.setFocused(focused); int i = this.children.indexOf(focused); if (i >= 0) { E entry2 = (E)this.children.get(i); this.setSelected(entry2); if (this.minecraft.getLastInputType().isKeyboard()) { this.ensureVisible(entry2); } } } @Nullable protected E nextEntry(ScreenDirection direction) { return this.nextEntry(direction, entry -> true); } @Nullable protected E nextEntry(ScreenDirection direction, Predicate predicate) { return this.nextEntry(direction, predicate, this.getSelected()); } @Nullable protected E nextEntry(ScreenDirection direction, Predicate predicate, @Nullable E selected) { int i = switch (direction) { case RIGHT, LEFT -> 0; case UP -> -1; case DOWN -> 1; }; if (!this.children().isEmpty() && i != 0) { int j; if (selected == null) { j = i > 0 ? 0 : this.children().size() - 1; } else { j = this.children().indexOf(selected) + i; } for (int k = j; k >= 0 && k < this.children.size(); k += i) { E entry = (E)this.children().get(k); if (predicate.test(entry)) { return entry; } } } return null; } protected void renderListItems(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) { int i = this.getRowLeft(); int j = this.getRowWidth(); int k = this.itemHeight - 4; int l = this.getItemCount(); for (int m = 0; m < l; m++) { int n = this.getRowTop(m); int o = this.getRowBottom(m); if (o >= this.getY() && n <= this.getBottom()) { this.renderItem(guiGraphics, mouseX, mouseY, partialTick, m, i, n, j, k); } } } protected void renderItem(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, int index, int left, int top, int width, int height) { E entry = this.getEntry(index); entry.renderBack(guiGraphics, index, top, left, width, height, mouseX, mouseY, Objects.equals(this.hovered, entry), partialTick); if (this.isSelectedItem(index)) { int i = this.isFocused() ? -1 : -8355712; this.renderSelection(guiGraphics, top, width, height, i, -16777216); } entry.render(guiGraphics, index, top, left, width, height, mouseX, mouseY, Objects.equals(this.hovered, entry), partialTick); } protected void renderSelection(GuiGraphics guiGraphics, int top, int width, int height, int outerColor, int innerColor) { int i = this.getX() + (this.width - width) / 2; int j = this.getX() + (this.width + width) / 2; guiGraphics.fill(i, top - 2, j, top + height + 2, outerColor); guiGraphics.fill(i + 1, top - 1, j - 1, top + height + 1, innerColor); } public int getRowLeft() { return this.getX() + this.width / 2 - this.getRowWidth() / 2 + 2; } public int getRowRight() { return this.getRowLeft() + this.getRowWidth(); } public int getRowTop(int index) { return this.getY() + 4 - (int)this.scrollAmount() + index * this.itemHeight + this.headerHeight; } public int getRowBottom(int index) { return this.getRowTop(index) + this.itemHeight; } public int getRowWidth() { return 220; } @Override public NarratableEntry.NarrationPriority narrationPriority() { if (this.isFocused()) { return NarratableEntry.NarrationPriority.FOCUSED; } else { return this.hovered != null ? NarratableEntry.NarrationPriority.HOVERED : NarratableEntry.NarrationPriority.NONE; } } @Nullable protected E remove(int index) { E entry = (E)this.children.get(index); return this.removeEntry((E)this.children.get(index)) ? entry : null; } protected boolean removeEntry(E entry) { boolean bl = this.children.remove(entry); if (bl && entry == this.getSelected()) { this.setSelected(null); } return bl; } @Nullable protected E getHovered() { return this.hovered; } void bindEntryToSelf(AbstractSelectionList.Entry entry) { entry.list = this; } protected void narrateListElementPosition(NarrationElementOutput narrationElementOutput, E entry) { List list = this.children(); if (list.size() > 1) { int i = list.indexOf(entry); if (i != -1) { narrationElementOutput.add(NarratedElementType.POSITION, Component.translatable("narrator.position.list", i + 1, list.size())); } } } @Environment(EnvType.CLIENT) protected abstract static class Entry> implements GuiEventListener { @Deprecated AbstractSelectionList list; @Override public void setFocused(boolean focused) { } @Override public boolean isFocused() { return this.list.getFocused() == this; } public abstract void render( GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean hovering, float partialTick ); public void renderBack( GuiGraphics guiGraphics, int index, int top, int left, int width, int height, int mouseX, int mouseY, boolean isMouseOver, float partialTick ) { } @Override public boolean isMouseOver(double mouseX, double mouseY) { return Objects.equals(this.list.getEntryAtPosition(mouseX, mouseY), this); } } @Environment(EnvType.CLIENT) class TrackedList extends AbstractList { private final List delegate = Lists.newArrayList(); public E get(int index) { return (E)this.delegate.get(index); } public int size() { return this.delegate.size(); } public E set(int index, E entry) { E entry2 = (E)this.delegate.set(index, entry); AbstractSelectionList.this.bindEntryToSelf(entry); return entry2; } public void add(int index, E entry) { this.delegate.add(index, entry); AbstractSelectionList.this.bindEntryToSelf(entry); } public E remove(int index) { return (E)this.delegate.remove(index); } } }