From b33d72f3eb00439e811b7227e5886e93e7b2fd52 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 10:34:31 +0800 Subject: [PATCH 01/45] Move Mods title button to fix proper tab order This aligns the tab order of the button to the natural order (left-right, top-down) of the buttons. --- .../client/gui/screens/TitleScreen.java.patch | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch b/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch index 3ba94479390..4c37b872e43 100644 --- a/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch +++ b/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/client/gui/screens/TitleScreen.java +++ b/net/minecraft/client/gui/screens/TitleScreen.java -@@ -104,11 +_,17 @@ +@@ -104,7 +_,7 @@ int copyrightWidth = this.font.width(COPYRIGHT_TEXT); int copyrightX = this.width - copyrightWidth - 2; int spacing = 24; @@ -9,16 +9,20 @@ if (this.minecraft.isDemo()) { topPos = this.createDemoMenuOptions(topPos, 24); } else { - topPos = this.createNormalMenuOptions(topPos, 24); -+ int modsOffset = SharedConstants.IS_RUNNING_IN_IDE ? 2 : -100; -+ int modsWidth = SharedConstants.IS_RUNNING_IN_IDE ? 98 : 200; -+ this.addRenderableWidget(new net.neoforged.neoforge.client.gui.widget.ModsButton(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(new net.neoforged.neoforge.client.gui.ModListScreen(this))) -+ .pos(this.width / 2 + modsOffset, topPos + 24).size(modsWidth, 20))); -+ if (!SharedConstants.IS_RUNNING_IN_IDE) -+ topPos += 24; // Move down Options, Quit, Language, and Accessibility buttons to make room for mods button (in-dev the test world button "handles" this) +@@ -112,6 +_,13 @@ } topPos = this.createTestWorldButton(topPos, 24); ++ // Neo: Add mods button to title screen ++ if (!SharedConstants.IS_RUNNING_IN_IDE) ++ topPos += 24; // Move down other buttons to make room for mods button (in-dev the test world button "handles" this) ++ int modsOffset = SharedConstants.IS_RUNNING_IN_IDE ? 2 : -100; ++ int modsWidth = SharedConstants.IS_RUNNING_IN_IDE ? 98 : 200; ++ this.addRenderableWidget(new net.neoforged.neoforge.client.gui.widget.ModsButton(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(new net.neoforged.neoforge.client.gui.ModListScreen(this))) ++ .pos(this.width / 2 + modsOffset, topPos).size(modsWidth, 20))); + SpriteIconButton language = this.addRenderableWidget( + CommonButtons.language( + 20, button -> this.minecraft.setScreen(new LanguageSelectScreen(this, this.minecraft.options, this.minecraft.getLanguageManager())), true @@ -158,7 +_,7 @@ Button.builder( Component.literal("Create Test World"), button -> CreateWorldScreen.testWorld(this.minecraft, () -> this.minecraft.setScreen(this)) From ab41809caecd30645e34bd6e34049f756a9edb10 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 10:38:17 +0800 Subject: [PATCH 02/45] Overhaul the mods list screen This overhaul has better visuals, better accessibility, and extensibility for mods to customize their mod entry on the list screen. --- .../client/gui/screens/PauseScreen.java.patch | 2 +- .../client/gui/screens/TitleScreen.java.patch | 2 +- .../neoforge/client/gui/ModListScreen.java | 483 ------------ .../gui/modlist/BlankModDisplayInfo.java | 72 ++ .../gui/modlist/DefaultModDisplayInfo.java | 149 ++++ .../client/gui/modlist/ImageResource.java | 62 ++ .../client/gui/modlist/ModDisplayInfo.java | 42 + .../client/gui/modlist/ModListScreen.java | 723 ++++++++++++++++++ .../client/gui/modlist/TestingResources.java | 231 ++++++ .../client/gui/modlist/package-info.java | 9 + .../widget/BackgroundWithPipingWidget.java | 60 ++ .../client/gui/widget/ModListWidget.java | 117 --- .../widget/ResizableTextureImageWidget.java | 85 ++ .../resources/assets/neoforge/lang/en_us.json | 18 + .../neoforge/textures/gui/bigsquirr.png | Bin 0 -> 29825 bytes src/main/resources/neoforged_icon.png | Bin 0 -> 1416 bytes src/main/resources/neoforged_pride.png | Bin 0 -> 1411 bytes src/main/resources/snowy_boi.png | Bin 0 -> 1205 bytes .../templates/minecraft.neoforge.mods.toml | 5 +- src/main/templates/neoforge.mods.toml | 10 +- 20 files changed, 1462 insertions(+), 608 deletions(-) delete mode 100644 src/client/java/net/neoforged/neoforge/client/gui/ModListScreen.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/BlankModDisplayInfo.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/package-info.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java delete mode 100644 src/client/java/net/neoforged/neoforge/client/gui/widget/ModListWidget.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java create mode 100644 src/main/resources/assets/neoforge/textures/gui/bigsquirr.png create mode 100644 src/main/resources/neoforged_icon.png create mode 100644 src/main/resources/neoforged_pride.png create mode 100644 src/main/resources/snowy_boi.png diff --git a/patches/net/minecraft/client/gui/screens/PauseScreen.java.patch b/patches/net/minecraft/client/gui/screens/PauseScreen.java.patch index 5bbe0ad9a8d..58a62b0e2eb 100644 --- a/patches/net/minecraft/client/gui/screens/PauseScreen.java.patch +++ b/patches/net/minecraft/client/gui/screens/PauseScreen.java.patch @@ -4,7 +4,7 @@ } else { helper.addChild(this.openScreenButton(PLAYER_REPORTING, () -> new SocialInteractionsScreen(this))); } -+ helper.addChild(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(new net.neoforged.neoforge.client.gui.ModListScreen(this))).width(BUTTON_WIDTH_FULL).build(), 2); ++ helper.addChild(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(net.neoforged.neoforge.client.gui.modlist.ModListScreen.create())).width(BUTTON_WIDTH_FULL).build(), 2); this.disconnectButton = helper.addChild( Button.builder( diff --git a/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch b/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch index 4c37b872e43..215625db8b5 100644 --- a/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch +++ b/patches/net/minecraft/client/gui/screens/TitleScreen.java.patch @@ -18,7 +18,7 @@ + topPos += 24; // Move down other buttons to make room for mods button (in-dev the test world button "handles" this) + int modsOffset = SharedConstants.IS_RUNNING_IN_IDE ? 2 : -100; + int modsWidth = SharedConstants.IS_RUNNING_IN_IDE ? 98 : 200; -+ this.addRenderableWidget(new net.neoforged.neoforge.client.gui.widget.ModsButton(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(new net.neoforged.neoforge.client.gui.ModListScreen(this))) ++ this.addRenderableWidget(new net.neoforged.neoforge.client.gui.widget.ModsButton(Button.builder(Component.translatable("fml.menu.mods"), button -> this.minecraft.setScreen(net.neoforged.neoforge.client.gui.modlist.ModListScreen.create())) + .pos(this.width / 2 + modsOffset, topPos).size(modsWidth, 20))); SpriteIconButton language = this.addRenderableWidget( CommonButtons.language( diff --git a/src/client/java/net/neoforged/neoforge/client/gui/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/ModListScreen.java deleted file mode 100644 index a0a5df96796..00000000000 --- a/src/client/java/net/neoforged/neoforge/client/gui/ModListScreen.java +++ /dev/null @@ -1,483 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.client.gui; - -import com.mojang.blaze3d.platform.NativeImage; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.textures.AddressMode; -import com.mojang.blaze3d.textures.FilterMode; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.ActiveTextCollector; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.TextAlignment; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.client.gui.components.LogoRenderer; -import net.minecraft.client.gui.components.ObjectSelectionList; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.renderer.texture.DynamicTexture; -import net.minecraft.client.renderer.texture.TextureManager; -import net.minecraft.locale.Language; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; -import net.minecraft.resources.Identifier; -import net.minecraft.server.packs.PackLocationInfo; -import net.minecraft.server.packs.PackResources; -import net.minecraft.server.packs.repository.Pack; -import net.minecraft.server.packs.repository.PackSource; -import net.minecraft.server.packs.resources.IoSupplier; -import net.minecraft.util.FormattedCharSequence; -import net.minecraft.util.StringUtil; -import net.minecraft.util.Util; -import net.neoforged.fml.ModContainer; -import net.neoforged.fml.ModList; -import net.neoforged.fml.VersionChecker; -import net.neoforged.fml.i18n.FMLTranslations; -import net.neoforged.fml.i18n.MavenVersionTranslator; -import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.neoforge.client.gui.widget.ModListWidget; -import net.neoforged.neoforge.client.gui.widget.ScrollPanel; -import net.neoforged.neoforge.common.CommonHooks; -import net.neoforged.neoforge.common.util.Size2i; -import net.neoforged.neoforge.resource.ResourcePackLoader; -import net.neoforged.neoforgespi.language.IModInfo; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.maven.artifact.versioning.ComparableVersion; -import org.jspecify.annotations.Nullable; - -public class ModListScreen extends Screen { - private static String stripControlCodes(String value) { - return StringUtil.stripColor(value); - } - - private static final Logger LOGGER = LogManager.getLogger(); - - private enum SortType implements Comparator { - NORMAL, - A_TO_Z { - @Override - protected int compare(String name1, String name2) { - return name1.compareTo(name2); - } - }, - Z_TO_A { - @Override - protected int compare(String name1, String name2) { - return name2.compareTo(name1); - } - }; - - Button button; - - protected int compare(String name1, String name2) { - return 0; - } - - @Override - public int compare(ModContainer o1, ModContainer o2) { - String name1 = stripControlCodes(o1.getModInfo().getDisplayName()).toLowerCase(Locale.ROOT); - String name2 = stripControlCodes(o2.getModInfo().getDisplayName()).toLowerCase(Locale.ROOT); - return compare(name1, name2); - } - - Component getButtonText() { - return Component.translatable("fml.menu.mods." + name().toLowerCase(Locale.ROOT)); - } - } - - private static final int PADDING = 6; - - private Screen parentScreen; - - private ModListWidget modList; - private InfoPanel modInfo; - private ModListWidget.ModEntry selected = null; - private int listWidth; - private List mods; - private final List unsortedMods; - private Button configButton, openModsFolderButton, doneButton; - - private int buttonMargin = 1; - private int numButtons = SortType.values().length; - private String lastFilterText = ""; - - private EditBox search; - - private boolean sorted = false; - private SortType sortType = SortType.NORMAL; - - public ModListScreen(Screen parentScreen) { - super(Component.translatable("fml.menu.mods.title")); - this.parentScreen = parentScreen; - this.unsortedMods = Collections.unmodifiableList(ModList.get().getSortedMods()); - this.mods = this.unsortedMods; - } - - class InfoPanel extends ScrollPanel { - @Nullable - private Identifier logoPath; - private Size2i logoDims = new Size2i(0, 0); - private List lines = Collections.emptyList(); - - InfoPanel(Minecraft mcIn, int widthIn, int heightIn, int topIn) { - super(mcIn, widthIn, heightIn, topIn, modList.getRight() + PADDING); - } - - void setInfo(List lines, Identifier logoPath, Size2i logoDims) { - this.logoPath = logoPath; - this.logoDims = logoDims; - this.lines = resizeContent(lines); - } - - void clearInfo() { - this.logoPath = null; - this.logoDims = new Size2i(0, 0); - this.lines = Collections.emptyList(); - } - - private List resizeContent(List lines) { - List ret = new ArrayList<>(); - for (String line : lines) { - if (line == null) { - ret.add(null); - continue; - } - - Component chat = CommonHooks.newChatWithLinks(line, false); - int maxTextLength = this.width - 12; - if (maxTextLength >= 0) { - ret.addAll(Language.getInstance().getVisualOrder(font.getSplitter().splitLines(chat, maxTextLength, Style.EMPTY))); - } - } - return ret; - } - - @Override - public int getContentHeight() { - int height = 50; - height += (lines.size() * font.lineHeight); - if (height < this.bottom - this.top - 8) - height = this.bottom - this.top - 8; - return height; - } - - @Override - protected int getScrollAmount() { - return font.lineHeight * 3; - } - - @Override - protected void drawPanel(GuiGraphicsExtractor guiGraphics, int entryRight, int relativeY, int mouseX, int mouseY) { - if (logoPath != null) { - // Draw the logo image inscribed in a rectangle with width entryWidth (minus some padding) and height 50 - int headerHeight = 50; - guiGraphics.blitInscribed(logoPath, left + PADDING, relativeY, width - (PADDING * 2), headerHeight, logoDims.width, logoDims.height, false, true); - relativeY += headerHeight + PADDING; - } - - for (FormattedCharSequence line : lines) { - if (line != null) { - guiGraphics.text(ModListScreen.this.font, line, left + PADDING, relativeY, 0xFFFFFFFF); - } - relativeY += font.lineHeight; - } - - final Style component = findTextLine(mouseX, mouseY); - if (component != null) { - guiGraphics.componentHoverEffect(ModListScreen.this.font, component, mouseX, mouseY); - } - } - - @Nullable - private Style findTextLine(final int mouseX, final int mouseY) { - if (!isMouseOver(mouseX, mouseY)) - return null; - - double offset = (mouseY - top - PADDING - border) + scrollDistance; - if (logoPath != null) { - offset -= 50; - } - if (offset <= 0) - return null; - - int lineIdx = (int) (offset / font.lineHeight); - if (lineIdx >= lines.size() || lineIdx < 0) - return null; - - FormattedCharSequence line = lines.get(lineIdx); - if (line != null) { - var styleFinder = new ActiveTextCollector.ClickableStyleFinder( - // TODO 1.21.11: The calculatin of Y needs to be validated, it should be relative to the vertical line origin - font, mouseX - left - border - 1, (int) (offset - (lineIdx * font.lineHeight))); - styleFinder.accept(TextAlignment.LEFT, 0, 0, line); - return styleFinder.result(); - } - return null; - } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { - final Style component = findTextLine((int) event.x(), (int) event.y()); - if (component != null && component.getClickEvent() != null) { - defaultHandleGameClickEvent(component.getClickEvent(), minecraft, ModListScreen.this); - return true; - } - return super.mouseClicked(event, doubleClick); - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; - } - - @Override - public void updateNarration(NarrationElementOutput output) {} - } - - @Override - public void init() { - for (var mod : mods) { - listWidth = Math.max(listWidth, getFontRenderer().width(mod.getModInfo().getDisplayName()) + 10); - listWidth = Math.max(listWidth, getFontRenderer().width(MavenVersionTranslator.artifactVersionToString(mod.getModInfo().getVersion())) + 5); - } - listWidth = Math.max(Math.min(listWidth, width / 3), 100); - listWidth += listWidth % numButtons != 0 ? (numButtons - listWidth % numButtons) : 0; - - int modInfoWidth = this.width - this.listWidth - (PADDING * 3); - int doneButtonWidth = Math.min(modInfoWidth, 200); - int y = this.height - 20 - PADDING; - int fullButtonHeight = PADDING + 20 + PADDING; - - doneButton = Button.builder(Component.translatable("gui.done"), b -> ModListScreen.this.onClose()).bounds(((listWidth + PADDING + this.width - doneButtonWidth) / 2), y, doneButtonWidth, 20).build(); - openModsFolderButton = Button.builder(Component.translatable("fml.menu.mods.openmodsfolder"), b -> Util.getPlatform().openFile(FMLPaths.MODSDIR.get().toFile())).bounds(6, y, this.listWidth, 20).build(); - y -= 20 + PADDING; - configButton = Button.builder(Component.translatable("fml.menu.mods.config"), b -> ModListScreen.this.displayModConfig()).bounds(6, y, this.listWidth, 20).build(); - y -= 14 + PADDING; - search = new EditBox(getFontRenderer(), PADDING, y, listWidth, 14, Component.translatable("fml.menu.mods.search")); - - this.modList = new ModListWidget(this, listWidth, fullButtonHeight, search.getY() - getFontRenderer().lineHeight - PADDING); - this.modList.setX(6); - this.modInfo = new InfoPanel(this.minecraft, modInfoWidth, this.height - PADDING - fullButtonHeight, PADDING); - - this.addRenderableWidget(modList); - this.addRenderableWidget(modInfo); - this.addRenderableWidget(search); - this.addRenderableWidget(doneButton); - this.addRenderableWidget(configButton); - this.addRenderableWidget(openModsFolderButton); - - search.setFocused(false); - search.setCanLoseFocus(true); - configButton.active = false; - - final int width = listWidth / numButtons; - int x = PADDING; - addRenderableWidget(SortType.NORMAL.button = Button.builder(SortType.NORMAL.getButtonText(), b -> resortMods(SortType.NORMAL)).bounds(x, PADDING, width - buttonMargin, 20).build()); - x += width + buttonMargin; - addRenderableWidget(SortType.A_TO_Z.button = Button.builder(SortType.A_TO_Z.getButtonText(), b -> resortMods(SortType.A_TO_Z)).bounds(x, PADDING, width - buttonMargin, 20).build()); - x += width + buttonMargin; - addRenderableWidget(SortType.Z_TO_A.button = Button.builder(SortType.Z_TO_A.getButtonText(), b -> resortMods(SortType.Z_TO_A)).bounds(x, PADDING, width - buttonMargin, 20).build()); - resortMods(SortType.NORMAL); - updateCache(); - } - - private void displayModConfig() { - if (selected == null) return; - try { - IConfigScreenFactory.getForMod(selected.getInfo()).map(f -> f.createScreen(selected.getContainer(), this)).ifPresent(newScreen -> this.minecraft.setScreen(newScreen)); - } catch (final Exception e) { - LOGGER.error("There was a critical issue trying to build the config GUI for {}", selected.getInfo().getModId(), e); - } - } - - @Override - public void tick() { - if (modList.getSelected() != selected) { - modList.setSelected(selected); - } - - if (!search.getValue().equals(lastFilterText)) { - reloadMods(); - sorted = false; - } - - if (!sorted) { - reloadMods(); - mods.sort(sortType); - modList.refreshList(); - if (selected != null) { - selected = modList.children().stream().filter(e -> e.getInfo() == selected.getInfo()).findFirst().orElse(null); - updateCache(); - } - sorted = true; - } - } - - public > void buildModList(Consumer modListViewConsumer, Function newEntry) { - mods.forEach(mod -> modListViewConsumer.accept(newEntry.apply(mod))); - } - - private void reloadMods() { - this.mods = this.unsortedMods.stream().filter(mi -> stripControlCodes(mi.getModInfo().getDisplayName()).toLowerCase(Locale.ROOT).contains(search.getValue().toLowerCase(Locale.ROOT))).collect(Collectors.toCollection(ArrayList::new)); - lastFilterText = search.getValue(); - } - - private void resortMods(SortType newSort) { - this.sortType = newSort; - - for (SortType sort : SortType.values()) { - if (sort.button != null) - sort.button.active = sortType != sort; - } - sorted = false; - } - - @Override - public void extractRenderState(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float partialTick) { - super.extractRenderState(guiGraphics, mouseX, mouseY, partialTick); - Component text = Component.translatable("fml.menu.mods.search"); - int x = modList.getX() + ((modList.getRight() - modList.getX()) / 2) - (getFontRenderer().width(text) / 2); - guiGraphics.text(getFontRenderer(), text.getVisualOrderText(), x, search.getY() - getFontRenderer().lineHeight, 0xFFFFFFFF, false); - } - - public Minecraft getMinecraftInstance() { - return minecraft; - } - - public Font getFontRenderer() { - return font; - } - - public void setSelected(ModListWidget.ModEntry entry) { - this.selected = entry; - updateCache(); - } - - private void updateCache() { - if (selected == null) { - this.configButton.active = false; - this.modInfo.clearInfo(); - return; - } - IModInfo selectedMod = selected.getInfo(); - this.configButton.active = IConfigScreenFactory.getForMod(selectedMod).isPresent(); - List lines = new ArrayList<>(); - VersionChecker.CheckResult vercheck = VersionChecker.getResult(selectedMod); - - @SuppressWarnings("resource") - Pair logoData; - - if (selectedMod.getModId().equals(Identifier.DEFAULT_NAMESPACE)) { - logoData = Pair.of(LogoRenderer.MINECRAFT_LOGO, new Size2i(LogoRenderer.LOGO_TEXTURE_WIDTH, LogoRenderer.LOGO_TEXTURE_HEIGHT)); - } else { - logoData = selectedMod.getLogoFile().map(logoFile -> { - TextureManager tm = this.minecraft.getTextureManager(); - final Pack.ResourcesSupplier resourcePack = ResourcePackLoader.getPackFor(selectedMod.getModId()) - .orElse(ResourcePackLoader.getPackFor("neoforge").orElseThrow(() -> new RuntimeException("Can't find neoforge, WHAT!"))); - try (PackResources packResources = resourcePack.openPrimary(new PackLocationInfo("mod/" + selectedMod.getModId(), Component.empty(), PackSource.BUILT_IN, Optional.empty()))) { - NativeImage logo = null; - IoSupplier logoResource = packResources.getRootResource(logoFile.split("[/\\\\]")); - if (logoResource != null) - logo = NativeImage.read(logoResource.get()); - if (logo != null) { - var textureId = Identifier.fromNamespaceAndPath("neoforge", "modlogo"); - tm.register(textureId, new DynamicTexture(textureId::toString, logo) { - @Override - public void upload() { - // Use custom "blur" value which controls texture filtering (nearest-neighbor vs linear) - // TODO 1.21.11: Unclear if this is the best way of setting linear/nearest filtering - var filter = selectedMod.getLogoBlur() ? FilterMode.LINEAR : FilterMode.NEAREST; - sampler = RenderSystem.getSamplerCache().getSampler(AddressMode.CLAMP_TO_EDGE, AddressMode.CLAMP_TO_EDGE, filter, filter, false); - super.upload(); - } - }); - - return Pair.of(textureId, new Size2i(logo.getWidth(), logo.getHeight())); - } - } catch (IOException | IllegalArgumentException e) {} - return Pair.of(null, new Size2i(0, 0)); - }).orElse(Pair.of(null, new Size2i(0, 0))); - } - - lines.add(selectedMod.getDisplayName()); - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.version", MavenVersionTranslator.artifactVersionToString(selectedMod.getVersion()))); - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.idstate", selectedMod.getModId(), "LOADED")); // TODO: remove mod loading stages from here too - - // Normalizing line endings to LF because it is currently not automatically handled for us. Descriptions are already normalized. - selectedMod.getConfig().getConfigElement("credits").ifPresent(credits -> lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.credits", credits).replace("\r\n", "\n"))); - selectedMod.getConfig().getConfigElement("authors").ifPresent(authors -> lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.authors", authors).replace("\r\n", "\n"))); - selectedMod.getConfig().getConfigElement("displayURL").ifPresent(displayURL -> lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.displayurl", displayURL).replace("\r\n", "\n"))); - if (selectedMod.getOwningFile() == null || selectedMod.getOwningFile().getMods().size() == 1) - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.nochildmods")); - else - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.childmods", selectedMod.getOwningFile().getMods().stream().map(IModInfo::getDisplayName).collect(Collectors.joining(",")))); - - if (vercheck.status() == VersionChecker.Status.OUTDATED || vercheck.status() == VersionChecker.Status.BETA_OUTDATED) - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.updateavailable", vercheck.url() == null ? "" : vercheck.url()).replace("\r\n", "\n")); - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.license", selectedMod.getOwningFile().getLicense()).replace("\r\n", "\n")); - lines.add(null); - lines.add(FMLTranslations.getPattern("fml.menu.mods.info.description." + selectedMod.getModId(), selectedMod::getDescription)); - - /* Removed because people bitched that this information was misleading. - lines.add(null); - if (FMLEnvironment.secureJarsEnabled) { - lines.add(ForgeI18getOwningFile().getFile().n.parseMessage("fml.menu.mods.info.signature", selectedMod.getOwningFile().getCodeSigningFingerprint().orElse(ForgeI18n.parseMessage("fml.menu.mods.info.signature.unsigned")))); - lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.trust", selectedMod.getOwningFile().getTrustData().orElse(ForgeI18n.parseMessage("fml.menu.mods.info.trust.noauthority")))); - } else { - lines.add(ForgeI18n.parseMessage("fml.menu.mods.info.securejardisabled")); - } - */ - - if ((vercheck.status() == VersionChecker.Status.OUTDATED || vercheck.status() == VersionChecker.Status.BETA_OUTDATED) && vercheck.changes().size() > 0) { - lines.add(null); - lines.add(FMLTranslations.parseMessage("fml.menu.mods.info.changelogheader")); - for (Entry entry : vercheck.changes().entrySet()) { - lines.add(" " + entry.getKey() + ":"); - lines.add(entry.getValue()); - lines.add(null); - } - } - - modInfo.setInfo(lines, logoData.getLeft(), logoData.getRight()); - } - - @Override - public void resize(int width, int height) { - String s = this.search.getValue(); - SortType sort = this.sortType; - ModListWidget.ModEntry selected = this.selected; - this.init(width, height); - this.search.setValue(s); - this.selected = selected; - if (!this.search.getValue().isEmpty()) - reloadMods(); - if (sort != SortType.NORMAL) - resortMods(sort); - updateCache(); - } - - @Override - public void onClose() { - this.minecraft.setScreen(this.parentScreen); - } -} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/BlankModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/BlankModDisplayInfo.java new file mode 100644 index 00000000000..6feea0679d0 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/BlankModDisplayInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import java.net.URI; +import net.minecraft.network.chat.Component; +import org.jspecify.annotations.Nullable; + +/// A blank [mod display info][ModDisplayInfo]. +final class BlankModDisplayInfo implements ModDisplayInfo { + public static final BlankModDisplayInfo INSTANCE = new BlankModDisplayInfo(); + + private BlankModDisplayInfo() {} + + @Override + public String id() { + return ""; + } + + @Override + public Component displayName() { + return Component.empty(); + } + + @Override + public String version() { + return ""; + } + + @Override + public Component authors() { + return Component.empty(); + } + + @Override + public Component credits() { + return Component.empty(); + } + + @Override + public Component description() { + return Component.empty(); + } + + @Override + public Component license() { + return Component.empty(); + } + + @Override + public @Nullable ImageResource logo() { + return null; + } + + @Override + public @Nullable ImageResource icon() { + return null; + } + + @Override + public @Nullable URI displayUrl() { + return null; + } + + @Override + public @Nullable URI issuesUrl() { + return null; + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java new file mode 100644 index 00000000000..d9d2e29450e --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import com.mojang.logging.LogUtils; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Objects; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.Identifier; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.i18n.FMLTranslations; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +public class DefaultModDisplayInfo implements ModDisplayInfo { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final ModContainer container; + + public DefaultModDisplayInfo(ModContainer container) { + this.container = container; + } + + @Override + public String id() { + return container.getModId(); + } + + @Override + public Component displayName() { + return Component.literal(container.getModInfo().getDisplayName()); + } + + @Override + public String version() { + return container.getModInfo().getVersion().toString(); + } + + @Override + public Component authors() { + return container.getModInfo().getConfig().getConfigElement("authors") + .map(Component::literal) + .orElseGet(Component::empty); + } + + @Override + public Component credits() { + return container.getModInfo().getConfig().getConfigElement("credits") + .map(Component::literal) + .orElseGet(Component::empty); + } + + @Override + public Component description() { + //noinspection UnstableApiUsage + return Component.translatable(FMLTranslations.getPattern("neoforge.screen.mods.info.description." + id(), container.getModInfo()::getDescription)); + } + + @Override + public Component license() { + MutableComponent licenseText = Component.literal(container.getModInfo().getOwningFile().getLicense()); + if (container.getModInfo().getOwningFile().getConfig().getConfigElement("licenseURL").orElse(null) instanceof String licenseURL) { + try { + final URI uri = new URI(licenseURL); + return licenseText.withStyle(style -> style + .withUnderlined(true) + .withClickEvent(new ClickEvent.OpenUrl(uri))); + } catch (URISyntaxException e) { + LOGGER.warn("Failed to create license URL {} for mod ID {}", licenseURL, id()); + } + } + return licenseText; + } + + @Override + @Nullable + public ImageResource logo() { + return container.getModInfo().getLogoFile().map(this::convertPath).orElse(null); + } + + @Override + @Nullable + public ImageResource icon() { + if (container.getModInfo().getConfig().getConfigElement("iconFile") + .or(() -> container.getModInfo().getOwningFile().getConfig().getConfigElement("iconFile")) + .orElse(null) instanceof String iconFile) { + return convertPath(iconFile); + } + return null; + } + + private ImageResource convertPath(String path) { + if (path.indexOf('#') > 0) { + // Contains a pound sign -- it's a root resource, with parts of "#" + String[] split = path.split("#", 2); + return ImageResource.packRoot(split[0], split[1]); + } else if (path.indexOf(Identifier.NAMESPACE_SEPARATOR) > 0) { + // Contains a colon, therefore an identifier -- it's a pack resource + return ImageResource.packAsset(Identifier.parse(path)); + } else { + // It's a root resource; get from the mod's resource pack + return ImageResource.packRoot("mod/" + id(), path); + } + } + + @Override + @Nullable + public URI displayUrl() { + return container.getModInfo().getConfig().getConfigElement("displayURL") + .map(URI::create) + .orElse(null); + } + + @Override + @Nullable + public URI issuesUrl() { + return container.getModInfo().getOwningFile().getConfig().getConfigElement("issueTrackerURL") + .map(URI::create) + .orElse(null); + } + + public ModContainer container() { + return container; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (DefaultModDisplayInfo) obj; + return Objects.equals(this.container, that.container); + } + + @Override + public int hashCode() { + return Objects.hash(container); + } + + @Override + public String toString() { + return "DefaultModDisplayInfo[" + container.getModId() + ']'; + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java new file mode 100644 index 00000000000..2a68092867b --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import java.io.InputStream; +import java.util.regex.Pattern; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.resources.IoSupplier; +import net.minecraft.server.packs.resources.Resource; +import net.minecraft.server.packs.resources.ResourceManager; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +/// An image resource. This is primarily used for [mod display info][ModDisplayInfo]. +public sealed interface ImageResource { + @Nullable IoSupplier get(ResourceManager resourceManager); + + static ImageResource packRoot(String packId, String path) { + return new PackRoot(packId, path); + } + + static ImageResource packAsset(Identifier id) { + return new PackAsset(id); + } + + @ApiStatus.Internal + record PackRoot(String packId, String path) implements ImageResource { + private static final Pattern PATH_SPLITTER = Pattern.compile("[/\\\\]"); + + public @Nullable IoSupplier get(ResourceManager resourceManager) { + // We are not responsible for closing the PackResources + //noinspection resource + PackResources packResources = resourceManager.listPacks().filter(resource -> resource.packId().equals(packId)).findAny().orElse(null); + if (packResources == null) return null; + return packResources.getRootResource(PATH_SPLITTER.split(path)); + } + + @Override + public String toString() { + return packId + "[" + path + "]"; + } + } + + @ApiStatus.Internal + record PackAsset(Identifier path) implements ImageResource { + @Override + public @Nullable IoSupplier get(ResourceManager resourceManager) { + Resource resource = resourceManager.getResource(path).orElse(null); + if (resource == null) return null; + return resource::open; + } + + @Override + public String toString() { + return path.toString(); + } + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java new file mode 100644 index 00000000000..791fb89fd37 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import java.net.URI; +import net.minecraft.network.chat.Component; +import net.neoforged.fml.IExtensionPoint; +import org.jspecify.annotations.Nullable; + +// TODO: reconsider if extension point is the best way to do this + +/// An extension point for information displayed on the [mod list screen][ModListScreen]. +public interface ModDisplayInfo extends IExtensionPoint { + String id(); + + Component displayName(); + + String version(); + + Component authors(); + + Component credits(); + + Component description(); + + Component license(); + + @Nullable + ImageResource logo(); // rendered as rectangle + + @Nullable + ImageResource icon(); // rendered as a square + + @Nullable + URI displayUrl(); + + @Nullable + URI issuesUrl(); +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java new file mode 100644 index 00000000000..0b0825c4104 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -0,0 +1,723 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import static net.minecraft.network.chat.Component.translatable; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import com.mojang.blaze3d.platform.NativeImage; +import com.mojang.logging.LogUtils; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Path; +import java.time.Month; +import java.time.MonthDay; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.CycleButton; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.FocusableTextWidget; +import net.minecraft.client.gui.components.ImageWidget; +import net.minecraft.client.gui.components.LogoRenderer; +import net.minecraft.client.gui.components.MultiLineTextWidget; +import net.minecraft.client.gui.components.ObjectSelectionList; +import net.minecraft.client.gui.components.ScrollableLayout; +import net.minecraft.client.gui.layouts.EqualSpacingLayout; +import net.minecraft.client.gui.layouts.FrameLayout; +import net.minecraft.client.gui.layouts.GridLayout; +import net.minecraft.client.gui.layouts.GridLayout.RowHelper; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.layouts.LinearLayout.Orientation; +import net.minecraft.client.gui.layouts.SpacerElement; +import net.minecraft.client.gui.screens.ConfirmLinkScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.contents.PlainTextContents; +import net.minecraft.resources.Identifier; +import net.minecraft.server.packs.resources.IoSupplier; +import net.minecraft.util.CommonLinks; +import net.minecraft.util.SpecialDates; +import net.minecraft.util.Util; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import net.neoforged.neoforge.client.gui.widget.BackgroundWithPipingWidget; +import net.neoforged.neoforge.client.gui.widget.ResizableTextureImageWidget; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.VisibleForTesting; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +@ApiStatus.Internal +public class ModListScreen extends Screen { + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final int SIDEBAR_CONTROLS_WIDTH = 150; + private static final int SIDEBAR_SORT_BUTTON_WIDTH = 50; + private static final int SIDEBAR_CONTROLS_HEIGHT = 16; + private static final int SIDEBAR_MODS_LIST_WIDTH = 150; + private static final int INFO_PANEL_WIDTH = 250; + private static final int INFO_PANEL_FRAME_PADDING = 2; + private static final int ICON_SIZE = 24; + + private final ImmutableList mods; + private final Path modsFolder; + private final ConfigurationScreenFactory configFactory; + + private final List allEntries; + private SortType currentSort = SortType.DEFAULT; + + @Nullable + private HeaderAndFooterLayout layout; + @Nullable + private EqualSpacingLayout fixedSidebarLayout; + @Nullable + private ModsList displayList; + private ModListScreen.@Nullable ModInfoPanel displayPanel; + @Nullable + private EditBox search; + + public static ModListScreen create() { + final Builder mods = ImmutableList.builder(); + + for (ModContainer container : ModList.get().getSortedMods()) { + final ModDisplayInfo displayInfo; + final String modId = container.getModId(); + if (modId.equalsIgnoreCase("minecraft") && !FMLEnvironment.isProduction()) { + // This is a special case in development because the Minecraft mods.toml information is hardcoded in FML + // TODO: remove in the future once FML is updated to match + displayInfo = new DefaultModDisplayInfo(container) { + @Override + public ImageResource icon() { + return ImageResource.packRoot("vanilla", "pack.png"); + } + + @Override + public ImageResource logo() { + return ImageResource.packAsset(LogoRenderer.MINECRAFT_LOGO); + } + + @Override + public Component authors() { + return Component.literal("Mojang Studios"); + } + + @Override + public Component license() { + return Component.literal("Minecraft End User License Agreement").withStyle(style -> style + .withUnderlined(true) + .withClickEvent(new ClickEvent.OpenUrl(CommonLinks.EULA))); + } + + @Override + public URI displayUrl() { + return URI.create("https://minecraft.net"); + } + + @Override + public URI issuesUrl() { + return URI.create("https://bugs.mojang.com"); + } + + @Override + public Component description() { + return Component.empty(); + } + }; + } else { + displayInfo = container.getCustomExtension(ModDisplayInfo.class) + .orElseGet(() -> new DefaultModDisplayInfo(container)); + } + mods.add(displayInfo); + } + + // TODO: remove before publish + if (!FMLEnvironment.isProduction()) { + mods.add(TestingResources.neoPride()); + mods.add(TestingResources.winterFox()); + mods.add(TestingResources.exercise()); + } + + ConfigurationScreenFactory configFactory = displayInfo -> { + final ModContainer container = ModList.get().getModContainerById(displayInfo.id()).orElse(null); + if (container == null) return null; + + final IConfigScreenFactory factory = container.getCustomExtension(IConfigScreenFactory.class).orElse(null); + if (factory == null) return null; + + return parentScreen -> factory.createScreen(container, parentScreen); + }; + + return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), configFactory); + } + + public ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory) { + super(translatable("neoforge.screen.mods.title")); + this.mods = mods; + this.modsFolder = modsFolder; + this.allEntries = new ArrayList<>(mods.size()); + this.configFactory = configFactory; + } + + @Override + protected void init() { + super.init(); + this.layout = new HeaderAndFooterLayout(this, 33, 38); + + // Header + layout.addTitleHeader(Component.translatable("neoforge.screen.mods.title"), this.font); + + // Footer + final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, Orientation.HORIZONTAL)); + footer.spacing(4).defaultCellSetting().paddingTop(5); + + footer.addChild(Button.builder(Component.translatable("neoforge.screen.mods.button.open_folder"), + _ -> Util.getPlatform().openPath(modsFolder)).build()); + footer.addChild(Button.builder(CommonComponents.GUI_BACK, _ -> ModListScreen.this.onClose()).build()); + + // Content + final GridLayout contentBase = layout.addToContents(new GridLayout()).columnSpacing(0); + contentBase.defaultCellSetting().alignVerticallyTop().alignHorizontallyCenter().padding(0); + final RowHelper contentBaseHelper = contentBase.createRowHelper(3); + + final LinearLayout sidebar = contentBaseHelper.addChild(LinearLayout.vertical()); + sidebar.spacing(2).defaultCellSetting().alignVerticallyTop(); + final LinearLayout main = contentBaseHelper.addChild(LinearLayout.vertical(), 2); + + this.fixedSidebarLayout = sidebar.addChild(new EqualSpacingLayout(SIDEBAR_CONTROLS_WIDTH, SIDEBAR_CONTROLS_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); + + this.search = this.fixedSidebarLayout.addChild(new EditBox(this.font, SIDEBAR_CONTROLS_WIDTH - SIDEBAR_SORT_BUTTON_WIDTH - 2, SIDEBAR_CONTROLS_HEIGHT, Component.translatable("neoforge.screen.mods.search"))); + this.search.setHint(Component.translatable("neoforge.screen.mods.search").withStyle(ChatFormatting.GRAY)); + this.search.setFocused(false); + this.search.setCanLoseFocus(true); + this.search.setResponder(_ -> this.updateModsList()); + + this.fixedSidebarLayout.addChild(CycleButton.builder(SortType::getName, this.currentSort) + .displayOnlyValue() + .withValues(SortType.values()) + // 20 is the default button height + .create(0, 0, SIDEBAR_SORT_BUTTON_WIDTH, SIDEBAR_CONTROLS_HEIGHT, translatable("neoforge.screen.mods.button.sort"), (_, newValue) -> { + this.currentSort = newValue; + this.updateModsList(); + })); + + this.fixedSidebarLayout.arrangeElements(); // Arrange to figure out the height + this.displayList = sidebar.addChild(new ModsList(this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight())); + + this.displayPanel = new ModInfoPanel(INFO_PANEL_WIDTH, this.layout.getContentHeight()); + main.addChild(this.displayPanel.getMainLayout()); + + this.allEntries.clear(); + for (ModDisplayInfo mod : mods) { + this.allEntries.add(this.displayList.new Entry(mod)); + } + this.updateModsList(); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + assert this.layout != null; + assert this.displayList != null; + assert this.displayPanel != null; + assert this.fixedSidebarLayout != null; + this.displayPanel.updateHeight(this.layout.getContentHeight()); + this.displayList.updateSizeAndPosition(SIDEBAR_MODS_LIST_WIDTH, this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight(), 0); + this.displayPanel.getMainLayout().arrangeElements(); + this.layout.arrangeElements(); + this.displayList.setScrollAmount(this.displayList.scrollAmount()); + } + + @Override + public void onClose() { + super.onClose(); + final TextureManager textureManager = this.minecraft.getTextureManager(); + for (ModsList.Entry entry : this.allEntries) { + if (entry.iconData != null) { + textureManager.release(entry.iconData.sprite); + } + } + assert this.displayPanel != null; + this.displayPanel.close(); + } + + private void updateModsList() { + if (this.displayList == null) return; + + final ArrayList entries = new ArrayList<>(this.allEntries); + this.currentSort.sort(entries); + assert this.search != null; + final String filter = this.search.getValue().toLowerCase(Locale.ROOT); + if (!filter.isEmpty()) { + entries.removeIf(entry -> !entry.displayInfo.displayName().getString().toLowerCase(Locale.ROOT).contains(filter)); + } + final ModsList.Entry selected = this.displayList.getSelected(); + this.displayList.replaceEntries(entries); + // Reselect the previously-selected if possible + if (entries.contains(selected)) { + this.displayList.setSelected(selected); + } + } + + private record ImageData(Identifier sprite, int width, int height) {} + + @Nullable + private ImageData loadImage(String type, String modId, ImageResource imageResource) { + final IoSupplier resource = imageResource.get(this.minecraft.getResourceManager()); + + if (resource == null) { + LOGGER.warn("Failed to find {} resource {} for mod ID {} as it did not exist", type, imageResource, modId); + return null; + } + + final NativeImage image; + try (InputStream imageStream = resource.get()) { + image = NativeImage.read(imageStream); + } catch (IOException e) { + LOGGER.warn("Failed to load {} resource {} for mod ID {}", type, imageResource, modId); + return null; + } + + final TextureManager textureManager = this.minecraft.getTextureManager(); + final Identifier sprite = Identifier.fromNamespaceAndPath("neoforge", "mod/" + type + "/" + modId); + textureManager.register(sprite, new DynamicTexture(sprite::toString, image)); + + return new ImageData(sprite, image.getWidth(), image.getHeight()); + } + + @VisibleForTesting + @FunctionalInterface + public interface ConfigurationScreenFactory { + // unary operator takes in previous screen (to return to later) + @Nullable + UnaryOperator create(ModDisplayInfo displayInfo); + } + + private class ModsList extends ObjectSelectionList { + public ModsList(int height) { + // minecraft, width, height, y, itemHeight + super(ModListScreen.this.minecraft, SIDEBAR_MODS_LIST_WIDTH, height, 0, ICON_SIZE + 4); + // 24 pixels for the icon, 4 pixels padding for top and bottom (2 pixels each) + } + + @Override + public void clearEntries() { + super.clearEntries(); + this.setSelected(null); + } + + @Override + public int getRowWidth() { + return this.getWidth() - 24; + } + + @Override + protected int scrollBarX() { + return this.getRowRight() + this.scrollbarWidth(); + } + + @Override + public void setSelected(ModsList.@Nullable Entry selectedEntry) { + super.setSelected(selectedEntry); + assert ModListScreen.this.displayPanel != null; + ModListScreen.this.displayPanel.updateSelected(selectedEntry); + } + + class Entry extends ObjectSelectionList.Entry { + final ModDisplayInfo displayInfo; + @Nullable + final ImageData iconData; + + Entry(ModDisplayInfo displayInfo) { + this.displayInfo = displayInfo; + if (displayInfo.icon() != null) { + this.iconData = ModListScreen.this.loadImage("icon", displayInfo.id(), Objects.requireNonNull(displayInfo.icon())); + } else { + this.iconData = null; + } + } + + @Override + public Component getNarration() { + return Component.translatable("narrator.select", Component.translatable("neoforge.screen.mods.list.narration", displayInfo.displayName(), displayInfo.version())); + } + + @Override + public void extractContent(GuiGraphicsExtractor graphics, int mouseX, int mouseY, boolean hovered, float a) { + int left = this.getContentX(); + int top = this.getContentY(); + int textLeft = left + 2; + + if (iconData != null) { + graphics.blit(RenderPipelines.GUI_TEXTURED, iconData.sprite, left, top, 0.0F, 0.0F, ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE); + textLeft += ICON_SIZE + 4; + } + + top += 4; // padding + int maxTextWidth = getRowWidth() - textLeft + left - 4; + + final Language language = Language.getInstance(); + graphics.text(ModListScreen.this.font, language.getVisualOrder(ModListScreen.this.font.ellipsize(displayInfo.displayName(), maxTextWidth)), textLeft, top, 0xFFFFFFFF); + top += ModListScreen.this.font.lineHeight; + graphics.text(ModListScreen.this.font, language.getVisualOrder(ModListScreen.this.font.ellipsize(Component.literal(displayInfo.version()), maxTextWidth)), textLeft, top, 0xFF000000 + Objects.requireNonNull(ChatFormatting.GRAY.getColor())); + } + } + } + + private class ModInfoPanel implements Closeable { + private static final MonthDay APRIL_FOOLS = MonthDay.of(Month.APRIL, 1); + private static final int MAIN_PADDING = 4; // Padding between sections + + private final int width; + private final ResizableTextureImageWidget logoWidget; + private final MultiLineTextWidget displayNameWidget; + private final MultiLineTextWidget idAndVersionWidget; + private final MultiLineTextWidget authorsWidget; + private final MultiLineTextWidget creditsWidget; + private final MultiLineTextWidget licenseWidget; + private final Button homepageButton; + private final Button issuesButton; + private final Button configButton; + private final MultiLineTextWidget descriptionWidget; + private ModsList.@Nullable Entry selected; + @Nullable + private ImageData logoData; + @Nullable + private UnaryOperator configScreenFactory; + + private static final int BUTTON_PANEL_HEIGHT = 26; + + private final LinearLayout mainLayout; + private final FrameLayout contentFrame; + private final BackgroundWithPipingWidget backgroundWithPipingWidget; + private final ScrollableLayout scrollableContentContainer; + + private final ImageWidget squirr; + + public ModInfoPanel(int width, int height) { + this.mainLayout = LinearLayout.vertical(); + this.mainLayout.defaultCellSetting().alignHorizontallyCenter(); + + this.contentFrame = this.mainLayout.addChild(new FrameLayout(width, height - BUTTON_PANEL_HEIGHT)); + + LinearLayout contentLayout = new LinearLayout(0, 0, Orientation.VERTICAL); + this.width = width; + + // Ensure layout width fills the whole width + contentLayout.addChild(SpacerElement.width(width)); + + this.logoWidget = contentLayout.addChild(new ResizableTextureImageWidget(0, 0, 0, 0, MissingTextureAtlasSprite.getLocation(), 0, 0)); + + contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); + + this.displayNameWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build()); + this.idAndVersionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build()); + this.idAndVersionWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + + contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); + + this.authorsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(false)); + this.authorsWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + this.creditsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(false)); + this.creditsWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + this.licenseWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(false)); + this.licenseWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + + contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); + + this.descriptionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(false)); + this.descriptionWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + + this.backgroundWithPipingWidget = this.contentFrame.addChild(new BackgroundWithPipingWidget( + ModListScreen.this.minecraft, + 0, 0, + INFO_PANEL_WIDTH + INFO_PANEL_FRAME_PADDING * 2, height - BUTTON_PANEL_HEIGHT + INFO_PANEL_FRAME_PADDING * 2), + this.contentFrame.newChildLayoutSettings().padding(-INFO_PANEL_FRAME_PADDING)); + + this.scrollableContentContainer = this.contentFrame.addChild( + new ScrollableLayout(ModListScreen.this.minecraft, contentLayout, height - BUTTON_PANEL_HEIGHT), + this.contentFrame.newChildLayoutSettings().alignVerticallyTop()); + + this.squirr = this.contentFrame.addChild(ImageWidget.texture( + 32, 30, + Identifier.fromNamespaceAndPath("neoforge", "textures/gui/bigsquirr.png"), + 32, 30), this.contentFrame.newChildLayoutSettings().alignVerticallyBottom().alignHorizontallyRight().paddingRight(16).paddingBottom(-1)); + + EqualSpacingLayout buttonsLayout = this.mainLayout.addChild(new EqualSpacingLayout(width, BUTTON_PANEL_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); + buttonsLayout.defaultChildLayoutSetting().alignVerticallyBottom(); + + final int buttonWidth = 70; + final int buttonHeight = Button.DEFAULT_HEIGHT; + + this.homepageButton = buttonsLayout.addChild(Button.builder(Component.translatable("neoforge.screen.mods.button.homepage"), + _ -> { + if (displayInfo().displayUrl() != null) { + ConfirmLinkScreen.confirmLinkNow(ModListScreen.this, Objects.requireNonNull(displayInfo().displayUrl())); + } + }).size(buttonWidth, buttonHeight).build()); + this.issuesButton = buttonsLayout.addChild(Button.builder(Component.translatable("neoforge.screen.mods.button.issues"), + _ -> { + if (displayInfo().issuesUrl() != null) { + ConfirmLinkScreen.confirmLinkNow(ModListScreen.this, Objects.requireNonNull(displayInfo().issuesUrl())); + } + }).size(buttonWidth, buttonHeight).build()); + this.configButton = buttonsLayout.addChild(Button.builder(Component.translatable("neoforge.screen.mods.button.config"), + _ -> { + if (this.configScreenFactory != null) { + ModListScreen.this.minecraft.setScreen(this.configScreenFactory.apply(ModListScreen.this)); + } + }).size(buttonWidth, buttonHeight).build()); + + this.reset(); + } + + public LinearLayout getMainLayout() { + return mainLayout; + } + + private ModDisplayInfo displayInfo() { + if (this.selected == null) return BlankModDisplayInfo.INSTANCE; + return this.selected.displayInfo; + } + + public void updateHeight(int height) { + int infoHeight = height - BUTTON_PANEL_HEIGHT; + this.contentFrame.setMinHeight(infoHeight); + this.backgroundWithPipingWidget.setHeight(infoHeight); + this.scrollableContentContainer.setMaxHeight(infoHeight); + } + + public void updateSelected(ModsList.@Nullable Entry newSelected) { + if (newSelected == this.selected) return; // No change in selection + this.selected = newSelected; + this.update(); + assert ModListScreen.this.layout != null; + assert ModListScreen.this.displayPanel != null; + ModListScreen.this.displayPanel.getMainLayout().arrangeElements(); + ModListScreen.this.layout.arrangeElements(); + ModListScreen.this.repositionElements(); + } + + /// Resets layout to a blank state. + private void reset() { + if (this.logoData != null) { + final TextureManager textureManager = ModListScreen.this.minecraft.getTextureManager(); + textureManager.release(logoData.sprite); + this.logoData = null; + } + this.logoWidget.updateResource(MissingTextureAtlasSprite.getLocation(), 0, 0); + + this.displayNameWidget.setMessage(Component.empty()); + this.displayNameWidget.visible = false; + this.displayNameWidget.setHeight(0); + this.idAndVersionWidget.setMessage(Component.empty()); + this.idAndVersionWidget.visible = false; + this.idAndVersionWidget.setHeight(0); + this.authorsWidget.setMessage(Component.empty()); + this.authorsWidget.visible = false; + this.authorsWidget.setHeight(0); + this.creditsWidget.setMessage(Component.empty()); + this.creditsWidget.visible = false; + this.creditsWidget.setHeight(0); + this.licenseWidget.setMessage(Component.empty()); + this.licenseWidget.visible = false; + this.licenseWidget.setHeight(0); + + this.homepageButton.active = false; + this.issuesButton.active = false; + this.configScreenFactory = null; + this.configButton.active = false; + this.descriptionWidget.setMessage(Component.empty()); + this.descriptionWidget.visible = false; + this.descriptionWidget.setHeight(0); + + this.squirr.visible = false; + } + + /// Updates the layout based on the selected info. + public void update() { + reset(); + if (this.selected == null) return; // Do nothing if nothing is selected + ModDisplayInfo displayInfo = this.selected.displayInfo; + + final ImageResource logoResource = displayInfo.logo(); + if (logoResource != null) { + // Load new logo data + if (displayInfo.id().equals("minecraft")) { + // Special-case for the 'minecraft' mod: render the logo using LogoRenderer + float scaleFactor = Math.min(1F, (float) width / LogoRenderer.LOGO_TEXTURE_WIDTH); + int logoWidth = (int) (LogoRenderer.LOGO_TEXTURE_WIDTH * scaleFactor); + this.logoWidget.useMinecraftLogo(logoWidth); + } else { + this.logoData = loadImage("logo", displayInfo.id(), logoResource); + if (this.logoData != null) { + float scaleFactor = Math.min(1F, (float) width / this.logoData.width()); + int logoWidth = (int) (this.logoData.width() * scaleFactor); + int logoHeight = (int) (this.logoData.height() * scaleFactor); + this.logoWidget.updateResource(this.logoData.sprite(), logoWidth, logoHeight); + } + } + } + + this.displayNameWidget.setMessage(displayInfo.displayName()); + this.displayNameWidget.visible = true; + this.idAndVersionWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.subtitle", + Component.literal(displayInfo.id()).withStyle(style -> style + .withUnderlined(true) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) + .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), + Component.literal(displayInfo.version()).withStyle(style -> style + .withUnderlined(true) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) + .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) + .withStyle(ChatFormatting.GRAY)); + this.idAndVersionWidget.visible = true; + + if (containsText(displayInfo.authors())) { + this.authorsWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.authors", + displayInfo.authors().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + this.authorsWidget.visible = true; + } + if (containsText(displayInfo.credits())) { + this.creditsWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.credits", + displayInfo.credits().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + this.creditsWidget.visible = true; + } + this.licenseWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.license", + displayInfo.license().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + this.licenseWidget.visible = true; + + this.homepageButton.active = displayInfo.displayUrl() != null; + this.issuesButton.active = displayInfo.issuesUrl() != null; + this.configScreenFactory = ModListScreen.this.configFactory.create(displayInfo); + this.configButton.active = this.configScreenFactory != null; + + // Hardcoded case from ModInfo + if (containsText(displayInfo.description()) && !displayInfo.description().getString().equals("MISSING DESCRIPTION")) { + this.descriptionWidget.setMessage(displayInfo.description()); + this.descriptionWidget.visible = true; + } + + if (displayInfo.id().equals("neoforge") && SpecialDates.dayNow().equals(APRIL_FOOLS)) { + this.squirr.visible = true; + } + } + + private static boolean containsText(Component component) { + return !component.getContents().equals(PlainTextContents.EMPTY) && !component.getString().isEmpty(); + } + + @Override + public void close() { + this.reset(); + } + } + + private static final Comparator COMPARATOR_BY_NAME = Comparator.comparing(c -> c.displayInfo.displayName().getString().toLowerCase(Locale.ROOT)); + + private enum SortType { + DEFAULT("neoforge.screen.mods.sort.default", _ -> {}), + A_TO_Z("neoforge.screen.mods.sort.a_to_z", list -> list.sort(COMPARATOR_BY_NAME)), + Z_TO_A("neoforge.screen.mods.sort.z_to_a", list -> list.sort(COMPARATOR_BY_NAME.reversed())); + + private final String translationKey; + private final Consumer> sorter; + + SortType(String translationKey, Consumer> sorter) { + this.translationKey = translationKey; + this.sorter = sorter; + } + + public Component getName() { + return Component.translatable(this.translationKey); + } + + public void sort(List list) { + sorter.accept(list); + } + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java new file mode 100644 index 00000000000..ba66643e4cd --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import java.net.URI; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.TextColor; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +// TODO: delete once testing is finished +@ApiStatus.Internal +class TestingResources { + static ModDisplayInfo winterFox() { + return new ModDisplayInfo() { + @Override + public String id() { + return "winterfox"; + } + + @Override + public Component displayName() { + //noinspection UnnecessaryUnicodeEscape + return Component.literal("\u2744 Winter Fox \u2744"); + } + + @Override + public String version() { + return "2024.12"; + } + + @Override + public Component authors() { + return Component.empty(); + } + + @Override + public Component credits() { + return Component.empty(); + } + + @Override + public Component description() { + return Component.translatable("%s \n %s", + Component.literal("test ".repeat(20)), + Component.literal("Click this link!").withStyle(style -> style + .applyFormat(ChatFormatting.UNDERLINE) + .withClickEvent(new ClickEvent.OpenUrl(URI.create("https://neoforged.net"))) + .withHoverEvent(new HoverEvent.ShowText(Component.literal("boop!"))))); + } + + @Override + public Component license() { + return Component.literal("BSD-0").withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal("PLS WORK.")))); + } + + @Nullable + @Override + public ImageResource logo() { + return null; + } + + @Override + public ImageResource icon() { + return ImageResource.packRoot("mod/neoforge", "snowy_boi.png"); + } + + @Nullable + @Override + public URI displayUrl() { + return null; + } + + @Nullable + @Override + public URI issuesUrl() { + return null; + } + }; + } + + static ModDisplayInfo neoPride() { + return new ModDisplayInfo() { + @Override + public String id() { + return "neo_pride"; + } + + @Override + public Component displayName() { + final MutableComponent base = Component.empty(); + TextColor[] colors = { + TextColor.fromRgb(0xaa212b), // red + TextColor.fromRgb(0xfb8918), // orange + TextColor.fromRgb(0xffe359), // yellow + TextColor.fromRgb(0x32d850), // green + TextColor.fromRgb(0x3894ff), // blue + TextColor.fromRgb(0x6e5cb8) // violet + }; + int index = 0; + for (char c : "NeoForged Pride".toCharArray()) { + if (c != ' ') { + TextColor color = colors[index++ % colors.length]; + base.append(Component.literal(String.valueOf(c)).withStyle(s -> s.withColor(color))); + } else { + base.append(Component.literal(" ")); + } + } + return base; + } + + @Override + public String version() { + return "2024.06"; + } + + @Override + public Component authors() { + return Component.empty(); + } + + @Override + public Component credits() { + return Component.empty(); + } + + @Override + public Component description() { + return Component.empty(); + } + + @Override + public Component license() { + return Component.literal("No rights reserved!"); + } + + @Nullable + @Override + public ImageResource logo() { + return null; + } + + @Override + public ImageResource icon() { + return ImageResource.packRoot("mod/neoforge", "neoforged_pride.png"); + } + + @Nullable + @Override + public URI displayUrl() { + return null; + } + + @Nullable + @Override + public URI issuesUrl() { + return null; + } + }; + } + + static ModDisplayInfo exercise() { + return new ModDisplayInfo() { + @Override + public String id() { + return "exercise"; + } + + @Override + public Component displayName() { + return Component.literal("HA".repeat(20)); + } + + @Override + public String version() { + return "01.02.03.04.05.06.07.08.09"; + } + + @Override + public Component authors() { + return Component.empty(); + } + + @Override + public Component credits() { + return Component.empty(); + } + + @Override + public Component description() { + return Component.empty(); + } + + @Override + public Component license() { + return Component.literal("Some rights reserved!"); + } + + @Nullable + @Override + public ImageResource logo() { + return null; + } + + @Nullable + @Override + public ImageResource icon() { + return null; + } + + @Nullable + @Override + public URI displayUrl() { + return null; + } + + @Nullable + @Override + public URI issuesUrl() { + return null; + } + }; + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/package-info.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/package-info.java new file mode 100644 index 00000000000..62cacae76f3 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/package-info.java @@ -0,0 +1,9 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@NullMarked +package net.neoforged.neoforge.client.gui.modlist; + +import org.jspecify.annotations.NullMarked; diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java new file mode 100644 index 00000000000..fccb4bdc125 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java @@ -0,0 +1,60 @@ +package net.neoforged.neoforge.client.gui.widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import org.jspecify.annotations.Nullable; + +/// Renders the background and top/bottom piping from [net.minecraft.client.gui.components.AbstractSelectionList]. +public class BackgroundWithPipingWidget extends AbstractWidget { + private static final Identifier INWORLD_MENU_LIST_BACKGROUND = Identifier.withDefaultNamespace("textures/gui/inworld_menu_list_background.png"); + private static final Identifier MENU_LIST_BACKGROUND = Identifier.withDefaultNamespace("textures/gui/menu_list_background.png"); + + @Nullable + private final Minecraft minecraft; + + public BackgroundWithPipingWidget(@Nullable Minecraft minecraft, int x, int y, int width, int height) { + super(x, y, width, height, Component.empty()); + this.minecraft = minecraft; + } + + public BackgroundWithPipingWidget(int x, int y, int width, int height) { + this(null, x, y, width, height); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return false; // Avoid capturing mouse events + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + Identifier headerSeparator = this.minecraft == null || this.minecraft.level == null ? Screen.INWORLD_HEADER_SEPARATOR : Screen.HEADER_SEPARATOR; + Identifier footerSeparator = this.minecraft == null || this.minecraft.level == null ? Screen.INWORLD_FOOTER_SEPARATOR : Screen.FOOTER_SEPARATOR; + graphics.blit(RenderPipelines.GUI_TEXTURED, headerSeparator, this.getX(), this.getY() - 2, 0.0F, 0.0F, this.getWidth(), 2, 32, 2); + graphics.blit(RenderPipelines.GUI_TEXTURED, footerSeparator, this.getX(), this.getBottom(), 0.0F, 0.0F, this.getWidth(), 2, 32, 2); + + Identifier menuListBackground = this.minecraft == null || this.minecraft.level == null ? INWORLD_MENU_LIST_BACKGROUND : MENU_LIST_BACKGROUND; + graphics.blit( + RenderPipelines.GUI_TEXTURED, + menuListBackground, + this.getX(), + this.getY(), + (float) this.getRight(), + (float) this.getBottom(), + this.getWidth(), + this.getHeight(), + 32, + 32); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + // No-op + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/ModListWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/ModListWidget.java deleted file mode 100644 index ad710cd36d2..00000000000 --- a/src/client/java/net/neoforged/neoforge/client/gui/widget/ModListWidget.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) Forge Development LLC and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.client.gui.widget; - -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiGraphicsExtractor; -import net.minecraft.client.gui.components.ObjectSelectionList; -import net.minecraft.client.input.MouseButtonEvent; -import net.minecraft.client.renderer.RenderPipelines; -import net.minecraft.locale.Language; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.FormattedText; -import net.minecraft.resources.Identifier; -import net.neoforged.fml.ModContainer; -import net.neoforged.fml.VersionChecker; -import net.neoforged.fml.i18n.MavenVersionTranslator; -import net.neoforged.neoforge.client.gui.ModListScreen; -import net.neoforged.neoforge.common.NeoForgeMod; -import net.neoforged.neoforgespi.language.IModInfo; - -public class ModListWidget extends ObjectSelectionList { - private static String stripControlCodes(String value) { - return net.minecraft.util.StringUtil.stripColor(value); - } - - private static final Identifier VERSION_CHECK_ICONS = Identifier.fromNamespaceAndPath(NeoForgeMod.MOD_ID, "textures/gui/version_check_icons.png"); - private final int listWidth; - - private ModListScreen parent; - - public ModListWidget(ModListScreen parent, int listWidth, int top, int bottom) { - super(parent.getMinecraftInstance(), listWidth, bottom - top, top, parent.getFontRenderer().lineHeight * 2 + 8); - this.parent = parent; - this.listWidth = listWidth; - //this.setRenderBackground(false); // TODO: Porting 1.20.5 still needed? - } - - @Override - protected int scrollBarX() { - return this.listWidth; - } - - @Override - public int getRowWidth() { - return this.listWidth; - } - - public void refreshList() { - this.clearEntries(); - parent.buildModList(this::addEntry, mod -> new ModEntry(mod, this.parent)); - } - - public class ModEntry extends ObjectSelectionList.Entry { - private final ModContainer container; - private final ModListScreen parent; - - ModEntry(ModContainer info, ModListScreen parent) { - this.container = info; - this.parent = parent; - } - - @Override - public Component getNarration() { - return Component.translatable("narrator.select", container.getModInfo().getDisplayName()); - } - - @Override - public void extractContent(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, boolean hovered, float partialTick) { - Component name = Component.literal(stripControlCodes(container.getModInfo().getDisplayName())); - Component version = Component.literal(stripControlCodes(MavenVersionTranslator.artifactVersionToString(container.getModInfo().getVersion()))); - VersionChecker.CheckResult vercheck = VersionChecker.getResult(container.getModInfo()); - int left = getContentX(); - int top = getContentY(); - Font font = this.parent.getFontRenderer(); - guiGraphics.text(font, Language.getInstance().getVisualOrder(FormattedText.composite(font.substrByWidth(name, listWidth))), left + 3, top + 2, 0xFFFFFFFF, false); - guiGraphics.text(font, Language.getInstance().getVisualOrder(FormattedText.composite(font.substrByWidth(version, listWidth))), left + 3, top + 2 + font.lineHeight, 0xFFCCCCCC, false); - if (vercheck.status().shouldDraw()) { - //TODO: Consider adding more icons for visualization - int entryHeight = getContentHeight(); - guiGraphics.blit(RenderPipelines.GUI_TEXTURED, VERSION_CHECK_ICONS, getX() + width - 12, top + entryHeight / 4, vercheck.status().getSheetOffset() * 8, (vercheck.status().isAnimated() && ((System.currentTimeMillis() / 800 & 1)) == 1) ? 8 : 0, 8, 8, 64, 16); - } - } - - @Override - public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) { - // clicking on a selected item a second time unselects it - parent.setSelected(isFocused() ? null : this); - ModListWidget.this.setSelected(isFocused() ? null : this); - return false; - } - - @Override - public void setFocused(boolean focused) { - // ignore focus loss so the item stays selected when tabbing to the config button - if (focused) { - parent.setSelected(this); - ModListWidget.this.setSelected(this); - } - } - - @Override - public boolean isFocused() { - return ModListWidget.this.getSelected() == this; - } - - public IModInfo getInfo() { - return container.getModInfo(); - } - - public ModContainer getContainer() { - return container; - } - } -} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java new file mode 100644 index 00000000000..c1bb0d85130 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java @@ -0,0 +1,85 @@ +package net.neoforged.neoforge.client.gui.widget; + +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.ImageWidget; +import net.minecraft.client.gui.components.LogoRenderer; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.resources.Identifier; +import org.jspecify.annotations.Nullable; + +/// A variation of [ImageWidget.Texture] for variable size textures, with support for the Minecraft (with edition) logo. +public class ResizableTextureImageWidget extends AbstractWidget { + private @Nullable LogoRenderer logoRenderer; + private Identifier texture; + private int textureWidth; + private int textureHeight; + + public ResizableTextureImageWidget(int x, int y, int width, int height, Identifier texture, int textureWidth, int textureHeight) { + super(x, y, width, height, CommonComponents.EMPTY); + this.texture = texture; + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) {} + + @Override + public void playDownSound(SoundManager soundManager) {} + + @Override + public boolean isActive() { + return false; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent navigationEvent) { + return null; + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + if (logoRenderer != null) { + graphics.pose().pushMatrix(); + // Slightly shrink the logo + float adjust = 6; + graphics.pose().translate(this.getX() + adjust / 2, this.getY()); + graphics.pose().scale(((float) LogoRenderer.LOGO_WIDTH - adjust) / LogoRenderer.LOGO_WIDTH); + logoRenderer.extractRenderState(graphics, this.getWidth(), 1.0F, 0); + graphics.pose().popMatrix(); + } else { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + this.texture, + this.getX(), + this.getY(), + 0.0F, + 0.0F, + this.getWidth(), + this.getHeight(), + this.textureWidth, + this.textureHeight); + } + } + + public void updateResource(Identifier identifier, int textureWidth, int textureHeight) { + this.texture = identifier; + this.textureWidth = textureWidth; + this.textureHeight = textureHeight; + this.logoRenderer = null; + this.setSize(textureWidth, textureHeight); + } + + public void useMinecraftLogo(int width) { + this.logoRenderer = new LogoRenderer(true); + this.textureWidth = width; + this.textureHeight = LogoRenderer.LOGO_HEIGHT + 7; // Padding for the edition overlap + this.setSize(textureWidth, textureHeight); + } +} diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 66c56caf5e4..7278b007ad7 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -48,6 +48,24 @@ "fml.button.open.mods.folder": "Open Mods Folder", "fml.button.continue.launch": "Proceed to main menu", + "neoforge.screen.mods.title": "Mods", + "neoforge.screen.mods.button.open_folder": "Open mods folder", + "neoforge.screen.mods.button.sort": "Sort", + "neoforge.screen.mods.sort.default": "Default", + "neoforge.screen.mods.sort.a_to_z": "A-Z", + "neoforge.screen.mods.sort.z_to_a": "Z-A", + "neoforge.screen.mods.search": "Search", + "neoforge.screen.mods.list.narration": "Mod %s with version %s", + "neoforge.screen.mods.info.subtitle": "mod ID %s, version %s", + "neoforge.screen.mods.list.subtitle.modid.click": "Click to copy mod ID", + "neoforge.screen.mods.list.subtitle.version.click": "Click to copy version", + "neoforge.screen.mods.info.authors": "Authors: %s", + "neoforge.screen.mods.info.credits": "Credits: %s", + "neoforge.screen.mods.info.license": "License: %s", + "neoforge.screen.mods.button.homepage": "Homepage", + "neoforge.screen.mods.button.issues": "Issues", + "neoforge.screen.mods.button.config": "Config", + "fml.modmismatchscreen.missingmods.client": "Your client is missing the following mods, install these mods to join this server:", "fml.modmismatchscreen.missingmods.server": "The server is missing the following mods, remove these mods from your client to join this server:", "fml.modmismatchscreen.mismatchedmods": "The following mod versions do not match, install the same version of these mods that the server has to join this server:", diff --git a/src/main/resources/assets/neoforge/textures/gui/bigsquirr.png b/src/main/resources/assets/neoforge/textures/gui/bigsquirr.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4f7072c43e8ad92fa3cf4c87225ff8e1dbb799 GIT binary patch literal 29825 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 s5=JNXpB=SFXCM9b$;rvfZ$&`=4^Q;;pF literal 0 HcmV?d00001 diff --git a/src/main/resources/neoforged_icon.png b/src/main/resources/neoforged_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e3c09ef3020ed496aa5669802aaa732de3bf4b4d GIT binary patch literal 1416 zcmY+D3p5jI9Kb)DW)aDAw6fd7<+xevHEbnpw=K_`$Cx7XxE{rXC~@8BCX1WodMBd{ zqm&a3p*C_?xiwT{!{&OgOZQkx&7D&@?)jbn_nq%M-~XKd`Tu|4B{!Etn(D^t002!g ziEu=@rz{5?rd%H%2;c*tav{vY!Hw+TfTEv@3JE(A41ig7Qucn*0fES^@~DR!9QG~qRLgO+O*7RbXE4f#sr1>v`l(?FyC$^;1y4?~!daPfNSBrz z)pyYFR&TqS6Mt`xPjN%G^DZ_F79fC?z3-IRP7P>$*l!ZN`%%+7@K>IqSK6JHu_sqnw#{8^@WCAwBDi z3VM;z>E1uwuOIAW2eG*B+yG*qng0_Gn=Y2H|EOV~aaBAz?&s?gQ!=J4@g_jX=~sT@ z!T!9XUFNx!N63%|ZdTZJm2S&(s?*$g>S;Md|Eg4NDRoFxc%geO)*Jz~@jtN`s0yEf zWEP(M#bn^LRCU%CN}=-otDw6}D_&8l3-AdM%(Bk6utn|YTL5+C1d*~vE6w%P+ZT64=$-_fnHihhH`@Pkbg79h0?wPQ3MIXuWghDFu(QwOTnB7|rnz z0W2+ZqqQZI`twv>V(ors1 zmPE&Cx9(9BgNFN8Ogl}dbTT?RJL<*<=~o$8@wm_W_tWuh_~KdlHf$DD>CN^EeE~Mg z)0#Q<{h5*C1)D}Xz5}MB4ZZ+*LftVnKW~EB!NKUv^*JY_bI&VmD3eP6zH!GGS{of1 zPCLRTHb&}1jL=`Vytzo}a(Y$La?Y8Z%MY%Xk9NihhH$5&xjBY43vu1CW3k}i3M>pP zIpMt@TSkh_XXbjvF#K>h2~8%o?~euj-qkW<^To4Un0u#QsDKr2bRrM~2D%X+j*9ha z2TjjXTa58BUhHE=+0wpBM~C}@FcD42m*N_oH`HIxXF02{E;t3a@t0H@*tg3v&m~%{ znHP4ic_0rCdC?Gw|2Pn9(#ks@h8BhjWs2I+a><9DfB#qPc9=o~CXAt-JVWzf$HjYc zg&)0(!qD}f?jo~uTh}%4BSIJvcx!BD$G85ZfMEe*VqXD+XyF`9_mjn3&;sxko*6=Y z-)a>}-G2_JvUBgo&}3b$(p#+x57T7hjV`u25!ng$4|n#J*A-EoM+S!$ug;zCY`2fB z8exP-6B*W7Qmc}~CzoF6(qumV((>zpgZ!d`ymy}sef#1Do)2MVm8{-|SNU4)z5w+V zRZAEZ-TROspZ@ieL#8l(0ePr&LBH+qYZlm|^1QTt4|?THQR={+J>~fR)1oSj%}OQ; z^pedAoIGzzMidUW5UesHw+$2hq3yuzQ?{bX7|j$xy_sh5W@fYqM(LmL>roZ4L-^A~7;_CVRbpydldPiBT zm=s@+?4dEaf{xUO8Tb5#c?ANM1P!42725I48Sy9d~*mDn%2OS oe?o2@B06#@=+cEndm~Ovp;_RKdEkC^8f$< literal 0 HcmV?d00001 diff --git a/src/main/resources/neoforged_pride.png b/src/main/resources/neoforged_pride.png new file mode 100644 index 0000000000000000000000000000000000000000..74cc0f71a4142d21d069289d64df0127d69cbb88 GIT binary patch literal 1411 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU~J8Fb`J1#c2+1T%1_J8No8Qr zm{>c}*5h!1NUMMF(xo9XZxoayHdx(sb(LA#<+>r@7S~#<8GF9)&oWWb)az@?K6r5c z(N)deo7eHJYf|{Z{OHYtB`=gz?@bqpI#TiQqusgP&p+I&ZMd+;=<1u9%pqruE_Lph zae94MV>ru0XO6`Z6KAYcH|~zCxOUz4YtY@J71PS^&%e4{#E0#N%OlHW(qbRkKAu^& zwD_KS--b;~1m+9-OO!Z1a$k2^Z-1elrOt;l9TR=e3%hq54H7>cd*)P|myNGXXh`p; z);XuSp6i~=+IsHR_b2*1;#;QjT+ifHZd6s7*u)`O8WPajD)ClIQ|(fT-1iUsmj6U{ zrWA!9II*!oD#W8t{ttiM`D;3UzPmghT3q;@bXz{zAwfLDH0Ss48{4AQ8~#h~xMcM& zN_fU!mAw})E^>GHZ&pxv_u>=V2Sw}>HJ4PKi&X4&7e=t$JO5(#rvnFc1)_?dU$Z;@ zl6k_?NZtDk?Cl3CIyYUny(jx--tGOiZ-4$}nE0`1^8&Zo6M^x_g3(ARg&i_fAC4 zQxoww>$=&uYr%}Yv%NYa*Vra!Gf!5$9P?|cq433rP4}P7S|}jOAOMH|wl!wm&@ivw z-*&wC-m}WB;m2=3PP%sYs%jcP`}Uh*%MTfzefsZhOVQt7n%sBQf*5~rFtEVkKK6*r z_3H~WPKJJ79$aKL(`WWE-Z1^!&t?_+7GK=9Z11}HTnrp=`17dw{|5_x{G0KWecQ7? zE#Dvg;#Xg*8@-^)^UNxy*FMR0^J7Ie-}GtCJQI?(cGh;)9D9$_eP=o@_HRFp?B5fQ z^uKWKy=Aj@uC|(2>O5pA4F_q9f|q-D)^dI2>AHFSO3RGe-NlDY!gG)NCm&w9J87F? zgueWZ&G+||oekd>%*X(T2}^Smrb-JQzRR6?dxiMZcF*!Yh*y-2)ZaP1mD zn}Wh4WrsG``eyAj-?L+t_@ARQ{_UDITi4mr{`sNTnp*HAqQH>yQT%qx8td}kc|09A zr~TTHdFIQJ88d9{ulumVV+s^!``AAm$liT9d-sh)FE$^yl)wCXzWti({z!@%{yfUg z-Kl0LdM9A>>J&W{tNXDRKC5c$Pd{_3XZQLQz_JkrCmh`$rMG0tEhA)y{IRczTJcuI o>E(wN>o2<_nG4K~3>xYG7(OPQE?E|T`89~|>FVdQ&MBb@09b@$D*ylh literal 0 HcmV?d00001 diff --git a/src/main/resources/snowy_boi.png b/src/main/resources/snowy_boi.png new file mode 100644 index 0000000000000000000000000000000000000000..197a8649600c23e6ded1667d695836fa50fc4cf7 GIT binary patch literal 1205 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9GG!XV7ZFl&wk0|QIC zr;B4q#hkZu{XIkqMUJPZ9CW_8%9Z^y|I33X6nK<5mv+l>Gc9y}vef?ji$#v=iu$hY zd5wW(Tvn~yw@$w18Np;I>Qkbta;c1)Kkm-Doj>oHJy9MaZ{h1_j``<6$xHAngY+2bePn>q1ce?lVK@P80S;J+u&!o=4>=$8> z;bP!pWpLMJejazGW8*RIvq?VMYquEbbA0*qD#C1ap0;8A$F|Jf3qnlRy6T2E&we!} zYG#J+SCbh?z5tuj7sWR3<$KZO;^3lAiK`!#UoI}?GW}Avfw?d3^_kmx>(yKBP8>}*3J~i&D%ht7*Z}DME3^I z#?~vBFI<}>U$1v;k)DLD&CigYlhey?C1OO^O#j!chBqqr9W;6I>Vd7{HPMT_SFGx+ zkH#Wvl78qPBb&xXOE0yFR=6*I{kLFs&2618*P4@TIy;UY78DUHgjAx=*cN ziK&K6N|FI4ph=%{!E}Yt}vef&j;nlB(7r+1TtTW#6PXn{HNk zJ^1IZMicSP{5CAm%vfR1zhR?^`0KAzkH0YAe0k+L@$x+f-|cgGe7yEQ$TjN1ERGti z#hbYwU3Te^58AaN><%b?S`Pc>M%fgybwsmI-uoprxBvax2FZpu{IKNnllPo2 Date: Tue, 7 Apr 2026 18:28:36 +0800 Subject: [PATCH 03/45] Adjust spacing to line up sidebar and info panel edges --- .../neoforge/client/gui/modlist/ModListScreen.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 0b0825c4104..ca724227cbf 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -205,8 +205,8 @@ protected void init() { contentBase.defaultCellSetting().alignVerticallyTop().alignHorizontallyCenter().padding(0); final RowHelper contentBaseHelper = contentBase.createRowHelper(3); - final LinearLayout sidebar = contentBaseHelper.addChild(LinearLayout.vertical()); - sidebar.spacing(2).defaultCellSetting().alignVerticallyTop(); + final LinearLayout sidebar = contentBaseHelper.addChild(LinearLayout.vertical(), contentBaseHelper.newCellSettings().paddingVertical(-INFO_PANEL_FRAME_PADDING)); + sidebar.spacing(4).defaultCellSetting().alignVerticallyTop(); final LinearLayout main = contentBaseHelper.addChild(LinearLayout.vertical(), 2); this.fixedSidebarLayout = sidebar.addChild(new EqualSpacingLayout(SIDEBAR_CONTROLS_WIDTH, SIDEBAR_CONTROLS_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); @@ -227,7 +227,7 @@ protected void init() { })); this.fixedSidebarLayout.arrangeElements(); // Arrange to figure out the height - this.displayList = sidebar.addChild(new ModsList(this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight())); + this.displayList = sidebar.addChild(new ModsList(this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 4)); this.displayPanel = new ModInfoPanel(INFO_PANEL_WIDTH, this.layout.getContentHeight()); main.addChild(this.displayPanel.getMainLayout()); @@ -249,7 +249,7 @@ protected void repositionElements() { assert this.displayPanel != null; assert this.fixedSidebarLayout != null; this.displayPanel.updateHeight(this.layout.getContentHeight()); - this.displayList.updateSizeAndPosition(SIDEBAR_MODS_LIST_WIDTH, this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight(), 0); + this.displayList.updateSizeAndPosition(SIDEBAR_MODS_LIST_WIDTH, this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 4, 0); this.displayPanel.getMainLayout().arrangeElements(); this.layout.arrangeElements(); this.displayList.setScrollAmount(this.displayList.scrollAmount()); From 21237994770ba9120e54457a08cafd050f290842 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 20:23:59 +0800 Subject: [PATCH 04/45] Cleanup formatting and license headers --- .../client/gui/modlist/ImageResource.java | 3 ++- .../client/gui/modlist/ModListScreen.java | 24 +++++++++---------- .../widget/BackgroundWithPipingWidget.java | 5 ++++ .../widget/ResizableTextureImageWidget.java | 5 ++++ 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java index 2a68092867b..a686b05b57f 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java @@ -17,7 +17,8 @@ /// An image resource. This is primarily used for [mod display info][ModDisplayInfo]. public sealed interface ImageResource { - @Nullable IoSupplier get(ResourceManager resourceManager); + @Nullable + IoSupplier get(ResourceManager resourceManager); static ImageResource packRoot(String packId, String path) { return new PackRoot(packId, path); diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index ca724227cbf..c2196e58b7b 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -509,9 +509,9 @@ public ModInfoPanel(int width, int height) { }); this.backgroundWithPipingWidget = this.contentFrame.addChild(new BackgroundWithPipingWidget( - ModListScreen.this.minecraft, - 0, 0, - INFO_PANEL_WIDTH + INFO_PANEL_FRAME_PADDING * 2, height - BUTTON_PANEL_HEIGHT + INFO_PANEL_FRAME_PADDING * 2), + ModListScreen.this.minecraft, + 0, 0, + INFO_PANEL_WIDTH + INFO_PANEL_FRAME_PADDING * 2, height - BUTTON_PANEL_HEIGHT + INFO_PANEL_FRAME_PADDING * 2), this.contentFrame.newChildLayoutSettings().padding(-INFO_PANEL_FRAME_PADDING)); this.scrollableContentContainer = this.contentFrame.addChild( @@ -642,15 +642,15 @@ public void update() { this.displayNameWidget.setMessage(displayInfo.displayName()); this.displayNameWidget.visible = true; this.idAndVersionWidget.setMessage(Component.translatable( - "neoforge.screen.mods.info.subtitle", - Component.literal(displayInfo.id()).withStyle(style -> style - .withUnderlined(true) - .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) - .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), - Component.literal(displayInfo.version()).withStyle(style -> style - .withUnderlined(true) - .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) - .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) + "neoforge.screen.mods.info.subtitle", + Component.literal(displayInfo.id()).withStyle(style -> style + .withUnderlined(true) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) + .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), + Component.literal(displayInfo.version()).withStyle(style -> style + .withUnderlined(true) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) + .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) .withStyle(ChatFormatting.GRAY)); this.idAndVersionWidget.visible = true; diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java index fccb4bdc125..b9eff138a02 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.client.gui.widget; import net.minecraft.client.Minecraft; diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java index c1bb0d85130..9563da46b21 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/ResizableTextureImageWidget.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.client.gui.widget; import net.minecraft.client.gui.ComponentPath; From 2310624b57d1a3dfdc2654b3d19187ad3f0c8a96 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 20:31:02 +0800 Subject: [PATCH 05/45] Align button panel to info panel edges --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index c2196e58b7b..d803e6c077f 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -524,7 +524,7 @@ public ModInfoPanel(int width, int height) { 32, 30), this.contentFrame.newChildLayoutSettings().alignVerticallyBottom().alignHorizontallyRight().paddingRight(16).paddingBottom(-1)); EqualSpacingLayout buttonsLayout = this.mainLayout.addChild(new EqualSpacingLayout(width, BUTTON_PANEL_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); - buttonsLayout.defaultChildLayoutSetting().alignVerticallyBottom(); + buttonsLayout.defaultChildLayoutSetting().alignVerticallyBottom().paddingHorizontal(-INFO_PANEL_FRAME_PADDING); final int buttonWidth = 70; final int buttonHeight = Button.DEFAULT_HEIGHT; From 47c6f0f371ffd32cab77dfb2c9e37b16205def38 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 20:38:06 +0800 Subject: [PATCH 06/45] Equalize spacing between action buttons and column spacing --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index d803e6c077f..9488c57d614 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -201,7 +201,7 @@ protected void init() { footer.addChild(Button.builder(CommonComponents.GUI_BACK, _ -> ModListScreen.this.onClose()).build()); // Content - final GridLayout contentBase = layout.addToContents(new GridLayout()).columnSpacing(0); + final GridLayout contentBase = layout.addToContents(new GridLayout()).columnSpacing(-3); contentBase.defaultCellSetting().alignVerticallyTop().alignHorizontallyCenter().padding(0); final RowHelper contentBaseHelper = contentBase.createRowHelper(3); @@ -526,7 +526,8 @@ public ModInfoPanel(int width, int height) { EqualSpacingLayout buttonsLayout = this.mainLayout.addChild(new EqualSpacingLayout(width, BUTTON_PANEL_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); buttonsLayout.defaultChildLayoutSetting().alignVerticallyBottom().paddingHorizontal(-INFO_PANEL_FRAME_PADDING); - final int buttonWidth = 70; + final int buttonSpacing = 4; + final int buttonWidth = width / 3 - (buttonSpacing / 2); final int buttonHeight = Button.DEFAULT_HEIGHT; this.homepageButton = buttonsLayout.addChild(Button.builder(Component.translatable("neoforge.screen.mods.button.homepage"), From 0ebf25c1839026d1c7b22b8f227d317a0cb651fe Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 20:54:00 +0800 Subject: [PATCH 07/45] Equalize spacing between action buttons and between footer buttons --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 9488c57d614..d263e30b60d 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -526,7 +526,7 @@ public ModInfoPanel(int width, int height) { EqualSpacingLayout buttonsLayout = this.mainLayout.addChild(new EqualSpacingLayout(width, BUTTON_PANEL_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); buttonsLayout.defaultChildLayoutSetting().alignVerticallyBottom().paddingHorizontal(-INFO_PANEL_FRAME_PADDING); - final int buttonSpacing = 4; + final int buttonSpacing = 3; final int buttonWidth = width / 3 - (buttonSpacing / 2); final int buttonHeight = Button.DEFAULT_HEIGHT; From 17ed192f62a86e62dd3b3e0283e97810e100ac3b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 20:55:18 +0800 Subject: [PATCH 08/45] Add padding above logo --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index d263e30b60d..4fd421063bf 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -433,7 +433,10 @@ public ModInfoPanel(int width, int height) { // Ensure layout width fills the whole width contentLayout.addChild(SpacerElement.width(width)); - this.logoWidget = contentLayout.addChild(new ResizableTextureImageWidget(0, 0, 0, 0, MissingTextureAtlasSprite.getLocation(), 0, 0)); + this.logoWidget = contentLayout.addChild( + new ResizableTextureImageWidget(0, 0, 0, 0, MissingTextureAtlasSprite.getLocation(), 0, 0), + contentLayout.newCellSettings().paddingTop(INFO_PANEL_FRAME_PADDING) + ); contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); From a3fa5255b8b765a8c830fc610f1143ad718bd1b7 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 21:16:18 +0800 Subject: [PATCH 09/45] Equalize spacing between sidebar and sidebar buttons --- .../neoforge/client/gui/modlist/ModListScreen.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 4fd421063bf..e06489d1138 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -206,12 +206,12 @@ protected void init() { final RowHelper contentBaseHelper = contentBase.createRowHelper(3); final LinearLayout sidebar = contentBaseHelper.addChild(LinearLayout.vertical(), contentBaseHelper.newCellSettings().paddingVertical(-INFO_PANEL_FRAME_PADDING)); - sidebar.spacing(4).defaultCellSetting().alignVerticallyTop(); + sidebar.spacing(6).defaultCellSetting().alignVerticallyTop(); final LinearLayout main = contentBaseHelper.addChild(LinearLayout.vertical(), 2); this.fixedSidebarLayout = sidebar.addChild(new EqualSpacingLayout(SIDEBAR_CONTROLS_WIDTH, SIDEBAR_CONTROLS_HEIGHT, EqualSpacingLayout.Orientation.HORIZONTAL)); - this.search = this.fixedSidebarLayout.addChild(new EditBox(this.font, SIDEBAR_CONTROLS_WIDTH - SIDEBAR_SORT_BUTTON_WIDTH - 2, SIDEBAR_CONTROLS_HEIGHT, Component.translatable("neoforge.screen.mods.search"))); + this.search = this.fixedSidebarLayout.addChild(new EditBox(this.font, SIDEBAR_CONTROLS_WIDTH - SIDEBAR_SORT_BUTTON_WIDTH - 4, SIDEBAR_CONTROLS_HEIGHT, Component.translatable("neoforge.screen.mods.search"))); this.search.setHint(Component.translatable("neoforge.screen.mods.search").withStyle(ChatFormatting.GRAY)); this.search.setFocused(false); this.search.setCanLoseFocus(true); @@ -227,7 +227,7 @@ protected void init() { })); this.fixedSidebarLayout.arrangeElements(); // Arrange to figure out the height - this.displayList = sidebar.addChild(new ModsList(this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 4)); + this.displayList = sidebar.addChild(new ModsList(this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 5)); this.displayPanel = new ModInfoPanel(INFO_PANEL_WIDTH, this.layout.getContentHeight()); main.addChild(this.displayPanel.getMainLayout()); @@ -249,7 +249,7 @@ protected void repositionElements() { assert this.displayPanel != null; assert this.fixedSidebarLayout != null; this.displayPanel.updateHeight(this.layout.getContentHeight()); - this.displayList.updateSizeAndPosition(SIDEBAR_MODS_LIST_WIDTH, this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 4, 0); + this.displayList.updateSizeAndPosition(SIDEBAR_MODS_LIST_WIDTH, this.layout.getContentHeight() - this.fixedSidebarLayout.getHeight() - 5, 0); this.displayPanel.getMainLayout().arrangeElements(); this.layout.arrangeElements(); this.displayList.setScrollAmount(this.displayList.scrollAmount()); From 1a220483f1359d7efb1f15a97ab49eba9367a72c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 21:21:07 +0800 Subject: [PATCH 10/45] Fix formatting --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index e06489d1138..5080bfdb89a 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -435,8 +435,7 @@ public ModInfoPanel(int width, int height) { this.logoWidget = contentLayout.addChild( new ResizableTextureImageWidget(0, 0, 0, 0, MissingTextureAtlasSprite.getLocation(), 0, 0), - contentLayout.newCellSettings().paddingTop(INFO_PANEL_FRAME_PADDING) - ); + contentLayout.newCellSettings().paddingTop(INFO_PANEL_FRAME_PADDING)); contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); From 5e660d3f862e80b97e6ac556226a030c58c06b96 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 21:45:20 +0800 Subject: [PATCH 11/45] Reduce horizontal padding for mod list --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 5080bfdb89a..2f9f08d101b 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -335,12 +335,12 @@ public void clearEntries() { @Override public int getRowWidth() { - return this.getWidth() - 24; + return this.getWidth() - 16; } @Override protected int scrollBarX() { - return this.getRowRight() + this.scrollbarWidth(); + return this.getRight() - this.scrollbarWidth(); } @Override From c4c7a95c665840587cfa215c59035804d080cdea Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 7 Apr 2026 21:45:30 +0800 Subject: [PATCH 12/45] Add extra testing resources --- .../neoforge/client/gui/modlist/ModListScreen.java | 6 +++++- .../neoforge/client/gui/modlist/TestingResources.java | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 2f9f08d101b..ae11ecf0fe1 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -160,7 +160,11 @@ public Component description() { if (!FMLEnvironment.isProduction()) { mods.add(TestingResources.neoPride()); mods.add(TestingResources.winterFox()); - mods.add(TestingResources.exercise()); + mods.add(TestingResources.exercise("exerciseA")); + mods.add(TestingResources.exercise("exerciseB")); + mods.add(TestingResources.exercise("exerciseC")); + mods.add(TestingResources.exercise("exerciseD")); + mods.add(TestingResources.exercise("exerciseE")); } ConfigurationScreenFactory configFactory = displayInfo -> { diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java index ba66643e4cd..de9bce075c2 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java @@ -166,16 +166,16 @@ public URI issuesUrl() { }; } - static ModDisplayInfo exercise() { + static ModDisplayInfo exercise(String modId) { return new ModDisplayInfo() { @Override public String id() { - return "exercise"; + return modId; } @Override public Component displayName() { - return Component.literal("HA".repeat(20)); + return Component.literal(modId + " " + "HA".repeat(10)); } @Override From adb00ac5ff5852ae9aff845d9b5bcb0e66b84f01 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:49:43 +0800 Subject: [PATCH 13/45] Resize sidebar buttons to default height --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index ae11ecf0fe1..727669bc2b6 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -79,7 +79,7 @@ public class ModListScreen extends Screen { private static final int SIDEBAR_CONTROLS_WIDTH = 150; private static final int SIDEBAR_SORT_BUTTON_WIDTH = 50; - private static final int SIDEBAR_CONTROLS_HEIGHT = 16; + private static final int SIDEBAR_CONTROLS_HEIGHT = Button.DEFAULT_HEIGHT; private static final int SIDEBAR_MODS_LIST_WIDTH = 150; private static final int INFO_PANEL_WIDTH = 250; private static final int INFO_PANEL_FRAME_PADDING = 2; From 8e8b78966f1346496c8c806a3dbfc3861fd9786c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:51:02 +0800 Subject: [PATCH 14/45] Remove default or mod load sort The default sort is now A-Z. --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 727669bc2b6..c27baef280a 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -90,7 +90,7 @@ public class ModListScreen extends Screen { private final ConfigurationScreenFactory configFactory; private final List allEntries; - private SortType currentSort = SortType.DEFAULT; + private SortType currentSort = SortType.A_TO_Z; @Nullable private HeaderAndFooterLayout layout; @@ -707,7 +707,6 @@ public void close() { private static final Comparator COMPARATOR_BY_NAME = Comparator.comparing(c -> c.displayInfo.displayName().getString().toLowerCase(Locale.ROOT)); private enum SortType { - DEFAULT("neoforge.screen.mods.sort.default", _ -> {}), A_TO_Z("neoforge.screen.mods.sort.a_to_z", list -> list.sort(COMPARATOR_BY_NAME)), Z_TO_A("neoforge.screen.mods.sort.z_to_a", list -> list.sort(COMPARATOR_BY_NAME.reversed())); From 8832f97ccff676d4abc9bbe78efc43c1ecfe35f7 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:51:53 +0800 Subject: [PATCH 15/45] Use boldface for authors, license, credits keys --- .../neoforge/client/gui/modlist/ModListScreen.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index c27baef280a..0a78b1dcd46 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -664,18 +664,18 @@ public void update() { if (containsText(displayInfo.authors())) { this.authorsWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.authors", - displayInfo.authors().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + displayInfo.authors().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); this.authorsWidget.visible = true; } if (containsText(displayInfo.credits())) { this.creditsWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.credits", - displayInfo.credits().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + displayInfo.credits().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); this.creditsWidget.visible = true; } this.licenseWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.license", - displayInfo.license().copy().withStyle(ChatFormatting.WHITE)).withStyle(ChatFormatting.GRAY)); + displayInfo.license().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); this.licenseWidget.visible = true; this.homepageButton.active = displayInfo.displayUrl() != null; From abc6186371155f1ffa0c791417e2a383708d9950 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:52:54 +0800 Subject: [PATCH 16/45] Move license widget before authors widget --- .../client/gui/modlist/ModListScreen.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 0a78b1dcd46..4c1d7a8657a 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -403,9 +403,9 @@ private class ModInfoPanel implements Closeable { private final ResizableTextureImageWidget logoWidget; private final MultiLineTextWidget displayNameWidget; private final MultiLineTextWidget idAndVersionWidget; + private final MultiLineTextWidget licenseWidget; private final MultiLineTextWidget authorsWidget; private final MultiLineTextWidget creditsWidget; - private final MultiLineTextWidget licenseWidget; private final Button homepageButton; private final Button issuesButton; private final Button configButton; @@ -462,37 +462,37 @@ public ModInfoPanel(int width, int height) { contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); - this.authorsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + this.licenseWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) .alwaysShowBorder(false) .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) .maxWidth(width) .build() .setCentered(false)); - this.authorsWidget.setComponentClickHandler(style -> { + this.licenseWidget.setComponentClickHandler(style -> { ClickEvent clickEvent = style.getClickEvent(); if (clickEvent != null) { defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); } }); - this.creditsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + this.authorsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) .alwaysShowBorder(false) .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) .maxWidth(width) .build() .setCentered(false)); - this.creditsWidget.setComponentClickHandler(style -> { + this.authorsWidget.setComponentClickHandler(style -> { ClickEvent clickEvent = style.getClickEvent(); if (clickEvent != null) { defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); } }); - this.licenseWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + this.creditsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) .alwaysShowBorder(false) .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) .maxWidth(width) .build() .setCentered(false)); - this.licenseWidget.setComponentClickHandler(style -> { + this.creditsWidget.setComponentClickHandler(style -> { ClickEvent clickEvent = style.getClickEvent(); if (clickEvent != null) { defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); @@ -600,15 +600,15 @@ private void reset() { this.idAndVersionWidget.setMessage(Component.empty()); this.idAndVersionWidget.visible = false; this.idAndVersionWidget.setHeight(0); + this.licenseWidget.setMessage(Component.empty()); + this.licenseWidget.visible = false; + this.licenseWidget.setHeight(0); this.authorsWidget.setMessage(Component.empty()); this.authorsWidget.visible = false; this.authorsWidget.setHeight(0); this.creditsWidget.setMessage(Component.empty()); this.creditsWidget.visible = false; this.creditsWidget.setHeight(0); - this.licenseWidget.setMessage(Component.empty()); - this.licenseWidget.visible = false; - this.licenseWidget.setHeight(0); this.homepageButton.active = false; this.issuesButton.active = false; @@ -661,6 +661,10 @@ public void update() { .withStyle(ChatFormatting.GRAY)); this.idAndVersionWidget.visible = true; + this.licenseWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.license", + displayInfo.license().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); + this.licenseWidget.visible = true; if (containsText(displayInfo.authors())) { this.authorsWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.authors", @@ -673,10 +677,6 @@ public void update() { displayInfo.credits().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); this.creditsWidget.visible = true; } - this.licenseWidget.setMessage(Component.translatable( - "neoforge.screen.mods.info.license", - displayInfo.license().copy().withStyle(ChatFormatting.WHITE).withStyle(style -> style.withBold(false))).withStyle(ChatFormatting.GRAY).withStyle(style -> style.withBold(true))); - this.licenseWidget.visible = true; this.homepageButton.active = displayInfo.displayUrl() != null; this.issuesButton.active = displayInfo.issuesUrl() != null; From 76c3e1faab2e873043b63e248a83208d7398602c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:56:53 +0800 Subject: [PATCH 17/45] Abbreviate to EULA --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 2 +- src/main/templates/minecraft.neoforge.mods.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 4c1d7a8657a..39cf85155af 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -129,7 +129,7 @@ public Component authors() { @Override public Component license() { - return Component.literal("Minecraft End User License Agreement").withStyle(style -> style + return Component.literal("Minecraft EULA").withStyle(style -> style .withUnderlined(true) .withClickEvent(new ClickEvent.OpenUrl(CommonLinks.EULA))); } diff --git a/src/main/templates/minecraft.neoforge.mods.toml b/src/main/templates/minecraft.neoforge.mods.toml index 69791d6a3a6..412362ed910 100644 --- a/src/main/templates/minecraft.neoforge.mods.toml +++ b/src/main/templates/minecraft.neoforge.mods.toml @@ -1,5 +1,5 @@ modLoader="minecraft" -license="Minecraft End User License Agreement" +license="Minecraft EULA" licenseURL="https://aka.ms/MinecraftEULA" [[mods]] modId="minecraft" From d1951594dda14c51fa19e984b72d168e8b2fcf20 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 01:59:03 +0800 Subject: [PATCH 18/45] Move testing exercise entries to end --- .../neoforge/client/gui/modlist/ModListScreen.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 39cf85155af..d0bcef7f434 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -160,11 +160,11 @@ public Component description() { if (!FMLEnvironment.isProduction()) { mods.add(TestingResources.neoPride()); mods.add(TestingResources.winterFox()); - mods.add(TestingResources.exercise("exerciseA")); - mods.add(TestingResources.exercise("exerciseB")); - mods.add(TestingResources.exercise("exerciseC")); - mods.add(TestingResources.exercise("exerciseD")); - mods.add(TestingResources.exercise("exerciseE")); + mods.add(TestingResources.exercise("ZexerciseA")); + mods.add(TestingResources.exercise("ZexerciseB")); + mods.add(TestingResources.exercise("ZexerciseC")); + mods.add(TestingResources.exercise("ZexerciseD")); + mods.add(TestingResources.exercise("ZexerciseE")); } ConfigurationScreenFactory configFactory = displayInfo -> { From 3c4205abfbd4176dbd1f0c6ea416a37501304acb Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 03:19:24 +0800 Subject: [PATCH 19/45] Dynamically change entries width based on scrollbar --- .../neoforge/client/gui/modlist/ModListScreen.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index d0bcef7f434..0508a1f5708 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -337,9 +337,20 @@ public void clearEntries() { this.setSelected(null); } + // Make the entries fit the container, and shrink if the scrollbar is present + @Override + public int getRowLeft() { + return this.getX() + 3; + } + @Override public int getRowWidth() { - return this.getWidth() - 16; + int rowWidth = this.getWidth() - 6; + if (this.scrollable()) { + // Shrink if scrollbar is present + rowWidth -= this.scrollbarWidth(); + } + return rowWidth; } @Override From 9b6a0aa29744e012298d8d36a7f7aa5ca4bc152e Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 03:52:58 +0800 Subject: [PATCH 20/45] Add separator before description The separator is only visible if there is a description to be rendered. --- .../client/gui/modlist/ModListScreen.java | 10 +- .../client/gui/widget/SolidColorWidget.java | 100 ++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 0508a1f5708..8393ec2ad39 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -58,6 +58,7 @@ import net.minecraft.network.chat.contents.PlainTextContents; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.resources.IoSupplier; +import net.minecraft.util.ARGB; import net.minecraft.util.CommonLinks; import net.minecraft.util.SpecialDates; import net.minecraft.util.Util; @@ -68,6 +69,7 @@ import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import net.neoforged.neoforge.client.gui.widget.BackgroundWithPipingWidget; import net.neoforged.neoforge.client.gui.widget.ResizableTextureImageWidget; +import net.neoforged.neoforge.client.gui.widget.SolidColorWidget; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.VisibleForTesting; import org.jspecify.annotations.Nullable; @@ -417,6 +419,7 @@ private class ModInfoPanel implements Closeable { private final MultiLineTextWidget licenseWidget; private final MultiLineTextWidget authorsWidget; private final MultiLineTextWidget creditsWidget; + private final SolidColorWidget separator; private final Button homepageButton; private final Button issuesButton; private final Button configButton; @@ -510,7 +513,10 @@ public ModInfoPanel(int width, int height) { } }); - contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); + this.separator = contentLayout.addChild( + new SolidColorWidget(width - 8, 1).setColor(ARGB.opaque(Objects.requireNonNull(ChatFormatting.GRAY.getColor()))).calculateShadow(), + contentLayout.newCellSettings().paddingVertical(MAIN_PADDING).alignHorizontallyCenter() + ); this.descriptionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) .alwaysShowBorder(false) @@ -625,6 +631,7 @@ private void reset() { this.issuesButton.active = false; this.configScreenFactory = null; this.configButton.active = false; + this.separator.visible = false; this.descriptionWidget.setMessage(Component.empty()); this.descriptionWidget.visible = false; this.descriptionWidget.setHeight(0); @@ -696,6 +703,7 @@ public void update() { // Hardcoded case from ModInfo if (containsText(displayInfo.description()) && !displayInfo.description().getString().equals("MISSING DESCRIPTION")) { + this.separator.visible = true; this.descriptionWidget.setMessage(displayInfo.description()); this.descriptionWidget.visible = true; } diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java new file mode 100644 index 00000000000..cf04e22537d --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java @@ -0,0 +1,100 @@ +package net.neoforged.neoforge.client.gui.widget; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.util.ARGB; + +/// Renders a solid color in the space of the widget, with drop shadow. On creation, the color and shadow color are transparent; use [#setColor(int)] and [#setShadowColor(int)] to change the colors. +/// +/// The drop shadow is rendered outside the widget bounds, alike to the drop shadow of text. +public class SolidColorWidget extends AbstractWidget { + private int color; + private int shadowColor; + + /// Creates a new instance of this widget. + /// + /// @param x the X position + /// @param y the Y position + /// @param width the width of the widget + /// @param height the height of the widget + public SolidColorWidget(int x, int y, int width, int height) { + super(x, y, width, height, Component.empty()); + } + + /// Creates a new instance of this widget. + /// + /// @param width the width of the widget + /// @param height the height of the widget + public SolidColorWidget(int width, int height) { + this(0, 0, width, height); + } + + /// Sets the main color. + /// + /// @param color the new main color + /// @return this instance, for chaining + @CanIgnoreReturnValue + public SolidColorWidget setColor(int color) { + this.color = color; + return this; + } + + /// {@return the main color} + public int getColor() { + return color; + } + + /// Sets the shadow color. + /// + /// @param shadowColor the new shadow color + /// @return this instance, for chaining + @CanIgnoreReturnValue + public SolidColorWidget setShadowColor(int shadowColor) { + this.shadowColor = shadowColor; + return this; + } + + /// {@return the shadow color} + public int getShadowColor() { + return shadowColor; + } + + /// Calculate the shadow color based off the main color. + /// + /// The calculation is the same used for the drop shadow of text: 25% of the original RGB color (alpha preserved). + /// + /// @return this instance, for chaining + @CanIgnoreReturnValue + public SolidColorWidget calculateShadow() { + return setShadowColor(ARGB.scaleRGB(this.getColor(), 0.25F)); + } + + @Override + protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) { + graphics.fill(RenderPipelines.GUI, getX() + 1, getY() + 1, getX() + 1 + getWidth(), getY() + 1 + getHeight(), shadowColor); + graphics.fill(RenderPipelines.GUI, getX(), getY(), getX() + getWidth(), getY() + getHeight(), color); + } + + @Override + protected void updateWidgetNarration(NarrationElementOutput output) { + } + + @Override + public boolean shouldTakeFocusAfterInteraction() { + return false; + } + + @Override + public boolean isActive() { + return false; // Never active + } + + @Override + protected void handleCursor(GuiGraphicsExtractor graphics) { + // Do nothing + } +} From 1717478860d2652b7f555731cd905823feb5c93b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Wed, 8 Apr 2026 06:11:58 +0800 Subject: [PATCH 21/45] Fix formatting and apply license header --- .../neoforge/client/gui/modlist/ModListScreen.java | 3 +-- .../neoforge/client/gui/widget/SolidColorWidget.java | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 8393ec2ad39..39626f33625 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -515,8 +515,7 @@ public ModInfoPanel(int width, int height) { this.separator = contentLayout.addChild( new SolidColorWidget(width - 8, 1).setColor(ARGB.opaque(Objects.requireNonNull(ChatFormatting.GRAY.getColor()))).calculateShadow(), - contentLayout.newCellSettings().paddingVertical(MAIN_PADDING).alignHorizontallyCenter() - ); + contentLayout.newCellSettings().paddingVertical(MAIN_PADDING).alignHorizontallyCenter()); this.descriptionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) .alwaysShowBorder(false) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java index cf04e22537d..448e7374674 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/SolidColorWidget.java @@ -1,3 +1,8 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + package net.neoforged.neoforge.client.gui.widget; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -80,8 +85,7 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouse } @Override - protected void updateWidgetNarration(NarrationElementOutput output) { - } + protected void updateWidgetNarration(NarrationElementOutput output) {} @Override public boolean shouldTakeFocusAfterInteraction() { From 07eb4e26dd00048348db1c1c299949ca62fdc4ec Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 9 Apr 2026 01:20:36 +0800 Subject: [PATCH 22/45] Set banner height to always 50px --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 +++-- .../neoforge/client/gui/modlist/TestingResources.java | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 39626f33625..2a9fd3178b2 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -86,6 +86,7 @@ public class ModListScreen extends Screen { private static final int INFO_PANEL_WIDTH = 250; private static final int INFO_PANEL_FRAME_PADDING = 2; private static final int ICON_SIZE = 24; + private static final int LOGO_MAX_HEIGHT = 50; private final ImmutableList mods; private final Path modsFolder; @@ -453,7 +454,7 @@ public ModInfoPanel(int width, int height) { this.logoWidget = contentLayout.addChild( new ResizableTextureImageWidget(0, 0, 0, 0, MissingTextureAtlasSprite.getLocation(), 0, 0), - contentLayout.newCellSettings().paddingTop(INFO_PANEL_FRAME_PADDING)); + contentLayout.newCellSettings().paddingTop(INFO_PANEL_FRAME_PADDING).alignHorizontallyCenter()); contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); @@ -655,7 +656,7 @@ public void update() { } else { this.logoData = loadImage("logo", displayInfo.id(), logoResource); if (this.logoData != null) { - float scaleFactor = Math.min(1F, (float) width / this.logoData.width()); + float scaleFactor = Math.min(1F, (float) LOGO_MAX_HEIGHT / this.logoData.height()); int logoWidth = (int) (this.logoData.width() * scaleFactor); int logoHeight = (int) (this.logoData.height() * scaleFactor); this.logoWidget.updateResource(this.logoData.sprite(), logoWidth, logoHeight); diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java index de9bce075c2..2c5cacefbe7 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java @@ -61,10 +61,9 @@ public Component license() { return Component.literal("BSD-0").withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal("PLS WORK.")))); } - @Nullable @Override public ImageResource logo() { - return null; + return ImageResource.packRoot("mod/neoforge", "snowy_boi.png"); } @Override From e7c87b854a5b4691994adff67b15aeb8c703750c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 9 Apr 2026 01:55:31 +0800 Subject: [PATCH 23/45] Fix loading of resources from multi-mod files --- .../client/gui/modlist/DefaultModDisplayInfo.java | 7 ++++++- .../neoforged/neoforge/resource/ResourcePackLoader.java | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java index d9d2e29450e..afa83151b73 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java @@ -14,7 +14,10 @@ import net.minecraft.network.chat.MutableComponent; import net.minecraft.resources.Identifier; import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; import net.neoforged.fml.i18n.FMLTranslations; +import net.neoforged.neoforge.resource.ResourcePackLoader; +import net.neoforged.neoforgespi.language.IModFileInfo; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -105,7 +108,9 @@ private ImageResource convertPath(String path) { return ImageResource.packAsset(Identifier.parse(path)); } else { // It's a root resource; get from the mod's resource pack - return ImageResource.packRoot("mod/" + id(), path); + IModFileInfo modFileInfo = ModList.get().getModFileById(id()); + String packId = ResourcePackLoader.getPackName(modFileInfo.getFile()); + return ImageResource.packRoot(packId, path); } } diff --git a/src/main/java/net/neoforged/neoforge/resource/ResourcePackLoader.java b/src/main/java/net/neoforged/neoforge/resource/ResourcePackLoader.java index 28bfe49853e..05df4d1cbde 100644 --- a/src/main/java/net/neoforged/neoforge/resource/ResourcePackLoader.java +++ b/src/main/java/net/neoforged/neoforge/resource/ResourcePackLoader.java @@ -104,7 +104,7 @@ private static void packFinder(Map modResource continue; } var modFileInfo = modFile.getModFileInfo(); - final String name = "mod/" + e.getKey().getModInfos().stream().map(IModInfo::getModId).collect(Collectors.joining(",")); + final String name = getPackName(modFile); final String version = e.getKey().getModInfos().stream().map(IModInfo::getVersion).map(ArtifactVersion::toString).collect(Collectors.joining(",")); final String packName = e.getKey().getFileName(); @@ -260,12 +260,16 @@ public static List getPackNames(PackType packType) { .filter(packType == PackType.CLIENT_RESOURCES ? IModFileInfo::showAsResourcePack : IModFileInfo::showAsDataPack) .map(IModFileInfo::getFile) .filter(ResourcePackLoader::hasResourcePack) - .map(mf -> "mod/" + mf.getModInfos().stream().map(IModInfo::getModId).collect(Collectors.joining())) + .map(ResourcePackLoader::getPackName) .toList()); ids.add(packType == PackType.CLIENT_RESOURCES ? MOD_RESOURCES_ID : MOD_DATA_ID); return ids; } + public static String getPackName(IModFile mf) { + return "mod/" + mf.getModInfos().stream().map(IModInfo::getModId).collect(Collectors.joining()); + } + private static boolean hasResourcePack(IModFile mf) { // Vanilla resources are already considered, so don't create another entry for the Minecraft mod-file return mf.getModInfos().stream().noneMatch(m -> "minecraft".equals(m.getModId())); From 39c595bfbcb46bf160951e5cc244ddaeab234177 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 9 Apr 2026 02:01:39 +0800 Subject: [PATCH 24/45] Constrain logo size to within panel width --- .../neoforge/client/gui/modlist/ModListScreen.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 2a9fd3178b2..eda8da3f6b2 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -86,7 +86,7 @@ public class ModListScreen extends Screen { private static final int INFO_PANEL_WIDTH = 250; private static final int INFO_PANEL_FRAME_PADDING = 2; private static final int ICON_SIZE = 24; - private static final int LOGO_MAX_HEIGHT = 50; + private static final int LOGO_HEIGHT = 50; private final ImmutableList mods; private final Path modsFolder; @@ -656,7 +656,9 @@ public void update() { } else { this.logoData = loadImage("logo", displayInfo.id(), logoResource); if (this.logoData != null) { - float scaleFactor = Math.min(1F, (float) LOGO_MAX_HEIGHT / this.logoData.height()); + float widthScaleFactor = Math.min(1F, (float) this.width / this.logoData.width()); + float heightScaleFactor = Math.min(1F, (float) LOGO_HEIGHT / this.logoData.height()); + float scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); int logoWidth = (int) (this.logoData.width() * scaleFactor); int logoHeight = (int) (this.logoData.height() * scaleFactor); this.logoWidget.updateResource(this.logoData.sprite(), logoWidth, logoHeight); From 317b0b43204ebfed14c9d2224e24a748e7ac64d7 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 9 Apr 2026 02:10:26 +0800 Subject: [PATCH 25/45] Deduplicate repeated text widget building and hiding --- .../client/gui/modlist/ModListScreen.java | 118 ++++++------------ 1 file changed, 36 insertions(+), 82 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index eda8da3f6b2..10de7ecfc3b 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -27,6 +27,7 @@ import java.util.function.UnaryOperator; import net.minecraft.ChatFormatting; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractStringWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.CycleButton; import net.minecraft.client.gui.components.EditBox; @@ -458,78 +459,21 @@ public ModInfoPanel(int width, int height) { contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); - this.displayNameWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build()); - this.idAndVersionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build()); - this.idAndVersionWidget.setComponentClickHandler(style -> { - ClickEvent clickEvent = style.getClickEvent(); - if (clickEvent != null) { - defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); - } - }); + this.displayNameWidget = buildTextWidget(contentLayout).setCentered(true); + this.displayNameWidget.setComponentClickHandler(null); // Disallow clicks for the display name + this.idAndVersionWidget = buildTextWidget(contentLayout).setCentered(true); contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); - this.licenseWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build() - .setCentered(false)); - this.licenseWidget.setComponentClickHandler(style -> { - ClickEvent clickEvent = style.getClickEvent(); - if (clickEvent != null) { - defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); - } - }); - this.authorsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build() - .setCentered(false)); - this.authorsWidget.setComponentClickHandler(style -> { - ClickEvent clickEvent = style.getClickEvent(); - if (clickEvent != null) { - defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); - } - }); - this.creditsWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build() - .setCentered(false)); - this.creditsWidget.setComponentClickHandler(style -> { - ClickEvent clickEvent = style.getClickEvent(); - if (clickEvent != null) { - defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); - } - }); + this.licenseWidget = buildTextWidget(contentLayout); + this.authorsWidget = buildTextWidget(contentLayout); + this.creditsWidget = buildTextWidget(contentLayout); this.separator = contentLayout.addChild( new SolidColorWidget(width - 8, 1).setColor(ARGB.opaque(Objects.requireNonNull(ChatFormatting.GRAY.getColor()))).calculateShadow(), contentLayout.newCellSettings().paddingVertical(MAIN_PADDING).alignHorizontallyCenter()); - this.descriptionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(width) - .build() - .setCentered(false)); - this.descriptionWidget.setComponentClickHandler(style -> { - ClickEvent clickEvent = style.getClickEvent(); - if (clickEvent != null) { - defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); - } - }); + this.descriptionWidget = buildTextWidget(contentLayout); this.backgroundWithPipingWidget = this.contentFrame.addChild(new BackgroundWithPipingWidget( ModListScreen.this.minecraft, @@ -575,6 +519,22 @@ public ModInfoPanel(int width, int height) { this.reset(); } + private MultiLineTextWidget buildTextWidget(LinearLayout layout) { + MultiLineTextWidget widget = layout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(false)); + widget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + return widget; + } + public LinearLayout getMainLayout() { return mainLayout; } @@ -611,34 +571,28 @@ private void reset() { } this.logoWidget.updateResource(MissingTextureAtlasSprite.getLocation(), 0, 0); - this.displayNameWidget.setMessage(Component.empty()); - this.displayNameWidget.visible = false; - this.displayNameWidget.setHeight(0); - this.idAndVersionWidget.setMessage(Component.empty()); - this.idAndVersionWidget.visible = false; - this.idAndVersionWidget.setHeight(0); - this.licenseWidget.setMessage(Component.empty()); - this.licenseWidget.visible = false; - this.licenseWidget.setHeight(0); - this.authorsWidget.setMessage(Component.empty()); - this.authorsWidget.visible = false; - this.authorsWidget.setHeight(0); - this.creditsWidget.setMessage(Component.empty()); - this.creditsWidget.visible = false; - this.creditsWidget.setHeight(0); + hideTextWidget(this.displayNameWidget); + hideTextWidget(this.idAndVersionWidget); + hideTextWidget(this.licenseWidget); + hideTextWidget(this.authorsWidget); + hideTextWidget(this.creditsWidget); this.homepageButton.active = false; this.issuesButton.active = false; this.configScreenFactory = null; this.configButton.active = false; this.separator.visible = false; - this.descriptionWidget.setMessage(Component.empty()); - this.descriptionWidget.visible = false; - this.descriptionWidget.setHeight(0); + hideTextWidget(this.descriptionWidget); this.squirr.visible = false; } + private void hideTextWidget(AbstractStringWidget widget) { + widget.setMessage(Component.empty()); + widget.visible = false; + widget.setHeight(0); + } + /// Updates the layout based on the selected info. public void update() { reset(); From 2887f3a608be2adc6ed55c841ae4a2dca5933dd2 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 9 Apr 2026 02:23:08 +0800 Subject: [PATCH 26/45] Set color mod ID and version to white --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 10de7ecfc3b..2a7438ff0a5 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -625,10 +625,12 @@ public void update() { this.idAndVersionWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.subtitle", Component.literal(displayInfo.id()).withStyle(style -> style + .withColor(ChatFormatting.WHITE) .withUnderlined(true) .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), Component.literal(displayInfo.version()).withStyle(style -> style + .withColor(ChatFormatting.WHITE) .withUnderlined(true) .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) From 216b92e2a621a1a4fd6fe4dc961b29a431c2124a Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Mon, 27 Apr 2026 23:17:21 +0800 Subject: [PATCH 27/45] Remove Mods title header --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 +---- src/main/resources/assets/neoforge/lang/en_us.json | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 2a7438ff0a5..1f9d85717aa 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -195,10 +195,7 @@ public ModListScreen(ImmutableList mods, Path modsFolder, Config @Override protected void init() { super.init(); - this.layout = new HeaderAndFooterLayout(this, 33, 38); - - // Header - layout.addTitleHeader(Component.translatable("neoforge.screen.mods.title"), this.font); + this.layout = new HeaderAndFooterLayout(this, 8, 38); // Footer final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, Orientation.HORIZONTAL)); diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index f269aeb2b0d..3bb49926dca 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -48,7 +48,6 @@ "fml.button.open.mods.folder": "Open Mods Folder", "fml.button.continue.launch": "Proceed to main menu", - "neoforge.screen.mods.title": "Mods", "neoforge.screen.mods.button.open_folder": "Open mods folder", "neoforge.screen.mods.button.sort": "Sort", "neoforge.screen.mods.sort.default": "Default", From 7e0e71f37defa31123e40dcd1efffcf3e3ed5d43 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 00:18:28 +0800 Subject: [PATCH 28/45] Prevent background with piping widget from being selectable --- .../gui/widget/BackgroundWithPipingWidget.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java index b9eff138a02..5dd7c3e9798 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/widget/BackgroundWithPipingWidget.java @@ -6,11 +6,14 @@ package net.neoforged.neoforge.client.gui.widget; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphicsExtractor; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.sounds.SoundManager; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import org.jspecify.annotations.Nullable; @@ -62,4 +65,17 @@ protected void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouse protected void updateWidgetNarration(NarrationElementOutput output) { // No-op } + + @Override + public void playDownSound(SoundManager soundManager) {} + + @Override + public boolean isActive() { + return false; + } + + @Override + public @Nullable ComponentPath nextFocusPath(FocusNavigationEvent navigationEvent) { + return null; + } } From 6967c4aa16cce7151712c624ac17447e300cb1e7 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 00:21:00 +0800 Subject: [PATCH 29/45] Simplify and change style for mod ID and version widget Both are now in gray, non-underlined, with the mod ID in italics. The "mod ID" and "version" part of the strings are removed. --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 +---- src/main/resources/assets/neoforge/lang/en_us.json | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 1f9d85717aa..09c64bc5fa3 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -622,13 +622,10 @@ public void update() { this.idAndVersionWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.subtitle", Component.literal(displayInfo.id()).withStyle(style -> style - .withColor(ChatFormatting.WHITE) - .withUnderlined(true) + .withItalic(true) .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), Component.literal(displayInfo.version()).withStyle(style -> style - .withColor(ChatFormatting.WHITE) - .withUnderlined(true) .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) .withStyle(ChatFormatting.GRAY)); diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 3bb49926dca..499135dd05b 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -55,7 +55,7 @@ "neoforge.screen.mods.sort.z_to_a": "Z-A", "neoforge.screen.mods.search": "Search", "neoforge.screen.mods.list.narration": "Mod %s with version %s", - "neoforge.screen.mods.info.subtitle": "mod ID %s, version %s", + "neoforge.screen.mods.info.subtitle": "%s %s", "neoforge.screen.mods.list.subtitle.modid.click": "Click to copy mod ID", "neoforge.screen.mods.list.subtitle.version.click": "Click to copy version", "neoforge.screen.mods.info.authors": "Authors: %s", From cb5bfa9bd96a524f8324e6ab2aed95f5f82a703c Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 00:25:39 +0800 Subject: [PATCH 30/45] Capitalize the mods folder open button --- src/main/resources/assets/neoforge/lang/en_us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 499135dd05b..6a77df8582d 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -48,7 +48,7 @@ "fml.button.open.mods.folder": "Open Mods Folder", "fml.button.continue.launch": "Proceed to main menu", - "neoforge.screen.mods.button.open_folder": "Open mods folder", + "neoforge.screen.mods.button.open_folder": "Open Mods Folder", "neoforge.screen.mods.button.sort": "Sort", "neoforge.screen.mods.sort.default": "Default", "neoforge.screen.mods.sort.a_to_z": "A-Z", From 04518632f7180e9a133097a08e4db47326226f58 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 00:35:15 +0800 Subject: [PATCH 31/45] Add tooltips for sort button values --- .../neoforge/client/gui/modlist/ModListScreen.java | 6 ++++++ src/main/resources/assets/neoforge/lang/en_us.json | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 09c64bc5fa3..dfea0f2942d 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -37,6 +37,7 @@ import net.minecraft.client.gui.components.MultiLineTextWidget; import net.minecraft.client.gui.components.ObjectSelectionList; import net.minecraft.client.gui.components.ScrollableLayout; +import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.layouts.EqualSpacingLayout; import net.minecraft.client.gui.layouts.FrameLayout; import net.minecraft.client.gui.layouts.GridLayout; @@ -225,6 +226,7 @@ protected void init() { this.fixedSidebarLayout.addChild(CycleButton.builder(SortType::getName, this.currentSort) .displayOnlyValue() .withValues(SortType.values()) + .withTooltip(sort -> Tooltip.create(sort.getHover())) // 20 is the default button height .create(0, 0, SIDEBAR_SORT_BUTTON_WIDTH, SIDEBAR_CONTROLS_HEIGHT, translatable("neoforge.screen.mods.button.sort"), (_, newValue) -> { this.currentSort = newValue; @@ -693,6 +695,10 @@ public Component getName() { return Component.translatable(this.translationKey); } + public Component getHover() { + return Component.translatable(this.translationKey + ".hover"); + } + public void sort(List list) { sorter.accept(list); } diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 6a77df8582d..a80b4824b5c 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -52,7 +52,9 @@ "neoforge.screen.mods.button.sort": "Sort", "neoforge.screen.mods.sort.default": "Default", "neoforge.screen.mods.sort.a_to_z": "A-Z", + "neoforge.screen.mods.sort.a_to_z.hover": "In alphabetical order by display name.", "neoforge.screen.mods.sort.z_to_a": "Z-A", + "neoforge.screen.mods.sort.z_to_a.hover": "In reverse alphabetical order by display name.", "neoforge.screen.mods.search": "Search", "neoforge.screen.mods.list.narration": "Mod %s with version %s", "neoforge.screen.mods.info.subtitle": "%s %s", From 02f4344759b3571eb18d5d0852caa9cf8246f444 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 01:16:03 +0800 Subject: [PATCH 32/45] Add support for version checker indicators and changelog --- .../client/gui/modlist/ModListScreen.java | 172 +++++++++++++++++- .../resources/assets/neoforge/lang/en_us.json | 6 + 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index dfea0f2942d..3a4639571a5 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.time.Month; import java.time.MonthDay; @@ -22,6 +23,7 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.UnaryOperator; @@ -66,12 +68,16 @@ import net.minecraft.util.Util; import net.neoforged.fml.ModContainer; import net.neoforged.fml.ModList; +import net.neoforged.fml.VersionChecker; +import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.fml.loading.FMLPaths; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import net.neoforged.neoforge.client.gui.widget.BackgroundWithPipingWidget; import net.neoforged.neoforge.client.gui.widget.ResizableTextureImageWidget; import net.neoforged.neoforge.client.gui.widget.SolidColorWidget; +import net.neoforged.neoforge.common.NeoForgeMod; +import org.apache.maven.artifact.versioning.ComparableVersion; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.VisibleForTesting; import org.jspecify.annotations.Nullable; @@ -93,6 +99,7 @@ public class ModListScreen extends Screen { private final ImmutableList mods; private final Path modsFolder; private final ConfigurationScreenFactory configFactory; + private final VersionCheckResultSupplier versionCheck; private final List allEntries; private SortType currentSort = SortType.A_TO_Z; @@ -182,15 +189,22 @@ public Component description() { return parentScreen -> factory.createScreen(container, parentScreen); }; - return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), configFactory); + VersionCheckResultSupplier versionCheck = modId -> ModList.get().getModContainerById(modId) + .map(ModContainer::getModInfo) + .map(VersionChecker::getResult) + .filter(result -> result.status() == VersionChecker.Status.OUTDATED || result.status() == VersionChecker.Status.BETA_OUTDATED) + .orElse(null); + + return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), configFactory, versionCheck); } - public ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory) { + public ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory, VersionCheckResultSupplier versionCheck) { super(translatable("neoforge.screen.mods.title")); this.mods = mods; this.modsFolder = modsFolder; this.allEntries = new ArrayList<>(mods.size()); this.configFactory = configFactory; + this.versionCheck = versionCheck; } @Override @@ -241,7 +255,7 @@ protected void init() { this.allEntries.clear(); for (ModDisplayInfo mod : mods) { - this.allEntries.add(this.displayList.new Entry(mod)); + this.allEntries.add(this.displayList.new Entry(this.versionCheck.get(mod.id()), mod)); } this.updateModsList(); @@ -327,6 +341,12 @@ public interface ConfigurationScreenFactory { UnaryOperator create(ModDisplayInfo displayInfo); } + @VisibleForTesting + @FunctionalInterface + public interface VersionCheckResultSupplier { + VersionChecker.@Nullable CheckResult get(String modId); + } + private class ModsList extends ObjectSelectionList { public ModsList(int height) { // minecraft, width, height, y, itemHeight @@ -369,11 +389,14 @@ public void setSelected(ModsList.@Nullable Entry selectedEntry) { } class Entry extends ObjectSelectionList.Entry { + private static final Identifier VERSION_CHECK_ICONS = Identifier.fromNamespaceAndPath(NeoForgeMod.MOD_ID, "textures/gui/version_check_icons.png"); + final VersionChecker.@Nullable CheckResult checkResult; final ModDisplayInfo displayInfo; @Nullable final ImageData iconData; - Entry(ModDisplayInfo displayInfo) { + Entry(VersionChecker.@Nullable CheckResult checkResult, ModDisplayInfo displayInfo) { + this.checkResult = checkResult; this.displayInfo = displayInfo; if (displayInfo.icon() != null) { this.iconData = ModListScreen.this.loadImage("icon", displayInfo.id(), Objects.requireNonNull(displayInfo.icon())); @@ -397,9 +420,24 @@ public void extractContent(GuiGraphicsExtractor graphics, int mouseX, int mouseY graphics.blit(RenderPipelines.GUI_TEXTURED, iconData.sprite, left, top, 0.0F, 0.0F, ICON_SIZE, ICON_SIZE, ICON_SIZE, ICON_SIZE); textLeft += ICON_SIZE + 4; } + int maxTextWidth = getRowWidth() - textLeft + left - 4; + + if (checkResult != null && checkResult.status().shouldDraw() && FMLConfig.getBoolConfigValue(FMLConfig.ConfigValue.VERSION_CHECK)) { + graphics.blit( + RenderPipelines.GUI_TEXTURED, + VERSION_CHECK_ICONS, + this.getContentRight() - 10, + this.getContentYMiddle() - (8 / 2), + checkResult.status().getSheetOffset() * 8, + (checkResult.status().isAnimated() && ((System.currentTimeMillis() / 800 & 1) == 1)) ? 8 : 0, + 8, + 8, + 64, + 16); + maxTextWidth -= 14; + } top += 4; // padding - int maxTextWidth = getRowWidth() - textLeft + left - 4; final Language language = Language.getInstance(); graphics.text(ModListScreen.this.font, language.getVisualOrder(ModListScreen.this.font.ellipsize(displayInfo.displayName(), maxTextWidth)), textLeft, top, 0xFFFFFFFF); @@ -417,6 +455,8 @@ private class ModInfoPanel implements Closeable { private final ResizableTextureImageWidget logoWidget; private final MultiLineTextWidget displayNameWidget; private final MultiLineTextWidget idAndVersionWidget; + private final MultiLineTextWidget newerVersionWidget; + private final Button newerVersionButton; private final MultiLineTextWidget licenseWidget; private final MultiLineTextWidget authorsWidget; private final MultiLineTextWidget creditsWidget; @@ -462,6 +502,27 @@ public ModInfoPanel(int width, int height) { this.displayNameWidget.setComponentClickHandler(null); // Disallow clicks for the display name this.idAndVersionWidget = buildTextWidget(contentLayout).setCentered(true); + this.newerVersionWidget = contentLayout.addChild(FocusableTextWidget.builder(Component.empty(), font, 2) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(width) + .build() + .setCentered(true), + contentLayout.newCellSettings().paddingVertical(3)); + this.newerVersionWidget.setComponentClickHandler(style -> { + ClickEvent clickEvent = style.getClickEvent(); + if (clickEvent != null) { + defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); + } + }); + this.newerVersionButton = contentLayout.addChild(Button.builder(translatable("Open update changelog"), + _ -> { + if (this.selected != null && this.selected.checkResult != null) { + this.openChangelogScreen(this.selected.checkResult); + } + }).build(), + contentLayout.newCellSettings().alignHorizontallyCenter()); + contentLayout.addChild(SpacerElement.height(MAIN_PADDING)); this.licenseWidget = buildTextWidget(contentLayout); @@ -572,6 +633,9 @@ private void reset() { hideTextWidget(this.displayNameWidget); hideTextWidget(this.idAndVersionWidget); + hideTextWidget(this.newerVersionWidget); + this.newerVersionButton.visible = this.newerVersionButton.active = false; + this.newerVersionButton.setHeight(0); hideTextWidget(this.licenseWidget); hideTextWidget(this.authorsWidget); hideTextWidget(this.creditsWidget); @@ -632,6 +696,18 @@ public void update() { .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) .withStyle(ChatFormatting.GRAY)); this.idAndVersionWidget.visible = true; + VersionChecker.CheckResult checkResult = this.selected.checkResult; + if (checkResult != null) { + this.newerVersionWidget.setMessage(Component.translatable( + "neoforge.screen.mods.info.update", + Component.literal(checkResult.target().toString()) + .withStyle(style -> style.withItalic(false))) + .withStyle(style -> style.withItalic(true))); + this.newerVersionWidget.visible = true; + + this.newerVersionButton.setHeight(Button.DEFAULT_HEIGHT); + this.newerVersionButton.visible = this.newerVersionButton.active = true; + } this.licenseWidget.setMessage(Component.translatable( "neoforge.screen.mods.info.license", @@ -667,6 +743,10 @@ public void update() { } } + private void openChangelogScreen(VersionChecker.CheckResult checkResult) { + ModListScreen.this.minecraft.setScreen(new ChangelogScreen(ModListScreen.this, displayInfo(), checkResult)); + } + private static boolean containsText(Component component) { return !component.getContents().equals(PlainTextContents.EMPTY) && !component.getString().isEmpty(); } @@ -677,6 +757,88 @@ public void close() { } } + private static class ChangelogScreen extends Screen { + private final @Nullable Screen lastScreen; + private final ModDisplayInfo info; + private final VersionChecker.CheckResult checkResult; + + @Nullable + private HeaderAndFooterLayout layout; + + protected ChangelogScreen(@Nullable Screen lastScreen, ModDisplayInfo info, VersionChecker.CheckResult checkResult) { + super(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName())); + this.lastScreen = lastScreen; + this.info = info; + this.checkResult = checkResult; + } + + @Override + protected void init() { + super.init(); + + this.layout = new HeaderAndFooterLayout(this); + this.layout.addTitleHeader(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName()), this.font); + + // Footer + final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, Orientation.HORIZONTAL)); + footer.spacing(4).defaultCellSetting().paddingTop(5); + + final Button updateSiteButton = footer.addChild(Button.builder(translatable("neoforge.screen.mods._changelog.open_site"), + _ -> clickUrlAction(minecraft, this, URI.create(this.checkResult.url()))).build()); + updateSiteButton.active = false; + if (checkResult.url() != null) { + String rawUrl = checkResult.url(); + try { + new URI(rawUrl); + updateSiteButton.active = true; + } catch (URISyntaxException exception) { + LOGGER.warn("Failed to create update site URI for mod {} update checker: {}", info.id(), rawUrl); + } + } + footer.addChild(Button.builder(CommonComponents.GUI_BACK, _ -> this.onClose()).build()); + + // Contents + final LinearLayout body = new LinearLayout(0, 0, Orientation.VERTICAL).spacing(4); + + if (checkResult.changes().isEmpty()) { + body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.no_changelog").withStyle(ChatFormatting.ITALIC), this.font) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(310) + .build() + .setCentered(false)); + } else { + for (Map.Entry updateEntry : checkResult.changes().entrySet()) { + body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.entry", + Component.literal(updateEntry.getKey().toString()).withStyle(ChatFormatting.BOLD), + Component.literal(updateEntry.getValue())), + this.font) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(310) + .build() + .setCentered(false)); + } + } + + layout.addToContents(new ScrollableLayout(this.minecraft, body, this.layout.getContentHeight())).setMinWidth(310); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + assert this.layout != null; + this.layout.arrangeElements(); + } + + @Override + public void onClose() { + this.minecraft.setScreen(lastScreen); + } + } + private static final Comparator COMPARATOR_BY_NAME = Comparator.comparing(c -> c.displayInfo.displayName().getString().toLowerCase(Locale.ROOT)); private enum SortType { diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index a80b4824b5c..13182664e54 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -57,6 +57,8 @@ "neoforge.screen.mods.sort.z_to_a.hover": "In reverse alphabetical order by display name.", "neoforge.screen.mods.search": "Search", "neoforge.screen.mods.list.narration": "Mod %s with version %s", + "neoforge.screen.mods.info.update": "Update available: %s", + "neoforge.screen.mods.info.update.click": "See changelog", "neoforge.screen.mods.info.subtitle": "%s %s", "neoforge.screen.mods.list.subtitle.modid.click": "Click to copy mod ID", "neoforge.screen.mods.list.subtitle.version.click": "Click to copy version", @@ -66,6 +68,10 @@ "neoforge.screen.mods.button.homepage": "Homepage", "neoforge.screen.mods.button.issues": "Issues", "neoforge.screen.mods.button.config": "Config", + "neoforge.screen.mods.changelog.title": "Update changelog for %s", + "neoforge.screen.mods.changelog.open_site": "Open update site", + "neoforge.screen.mods.changelog.no_changelog": "No changelog provided.", + "neoforge.screen.mods.changelog.entry": "%s: %s", "fml.modmismatchscreen.missingmods.client": "Your client is missing the following mods, install these mods to join this server:", "fml.modmismatchscreen.missingmods.server": "The server is missing the following mods, remove these mods from your client to join this server:", From cc28a9a34b067894b77813005f0a61d381df3d37 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 01:19:46 +0800 Subject: [PATCH 33/45] Extract static nested classes from ModListScreen --- .../client/gui/modlist/ChangelogScreen.java | 112 ++++++++++++++++++ .../modlist/ConfigurationScreenFactory.java | 19 +++ .../client/gui/modlist/ModListScreen.java | 102 +--------------- .../modlist/VersionCheckResultSupplier.java | 16 +++ 4 files changed, 148 insertions(+), 101 deletions(-) create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java create mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java new file mode 100644 index 00000000000..1eb230d41a6 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import static net.minecraft.network.chat.Component.translatable; + +import com.mojang.logging.LogUtils; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.FocusableTextWidget; +import net.minecraft.client.gui.components.ScrollableLayout; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.layouts.LinearLayout; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.neoforged.fml.VersionChecker; +import org.apache.maven.artifact.versioning.ComparableVersion; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +@ApiStatus.Internal +class ChangelogScreen extends Screen { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final @Nullable Screen lastScreen; + private final ModDisplayInfo info; + private final VersionChecker.CheckResult checkResult; + + @Nullable + private HeaderAndFooterLayout layout; + + protected ChangelogScreen(@Nullable Screen lastScreen, ModDisplayInfo info, VersionChecker.CheckResult checkResult) { + super(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName())); + this.lastScreen = lastScreen; + this.info = info; + this.checkResult = checkResult; + } + + @Override + protected void init() { + super.init(); + + this.layout = new HeaderAndFooterLayout(this); + this.layout.addTitleHeader(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName()), this.font); + + // Footer + final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, LinearLayout.Orientation.HORIZONTAL)); + footer.spacing(4).defaultCellSetting().paddingTop(5); + + final Button updateSiteButton = footer.addChild(Button.builder(translatable("neoforge.screen.mods._changelog.open_site"), + _ -> clickUrlAction(minecraft, this, URI.create(this.checkResult.url()))).build()); + updateSiteButton.active = false; + if (checkResult.url() != null) { + String rawUrl = checkResult.url(); + try { + new URI(rawUrl); + updateSiteButton.active = true; + } catch (URISyntaxException exception) { + LOGGER.warn("Failed to create update site URI for mod {} update checker: {}", info.id(), rawUrl); + } + } + footer.addChild(Button.builder(CommonComponents.GUI_BACK, _ -> this.onClose()).build()); + + // Contents + final LinearLayout body = new LinearLayout(0, 0, LinearLayout.Orientation.VERTICAL).spacing(4); + + if (checkResult.changes().isEmpty()) { + body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.no_changelog").withStyle(ChatFormatting.ITALIC), this.font) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(310) + .build() + .setCentered(false)); + } else { + for (Map.Entry updateEntry : checkResult.changes().entrySet()) { + body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.entry", + Component.literal(updateEntry.getKey().toString()).withStyle(ChatFormatting.BOLD), + Component.literal(updateEntry.getValue())), + this.font) + .alwaysShowBorder(false) + .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) + .maxWidth(310) + .build() + .setCentered(false)); + } + } + + layout.addToContents(new ScrollableLayout(this.minecraft, body, this.layout.getContentHeight())).setMinWidth(310); + + this.layout.visitWidgets(this::addRenderableWidget); + this.repositionElements(); + } + + @Override + protected void repositionElements() { + assert this.layout != null; + this.layout.arrangeElements(); + } + + @Override + public void onClose() { + this.minecraft.setScreen(lastScreen); + } +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java new file mode 100644 index 00000000000..aea86171b33 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import java.util.function.UnaryOperator; +import net.minecraft.client.gui.screens.Screen; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +@FunctionalInterface +@ApiStatus.Internal +public interface ConfigurationScreenFactory { + // unary operator takes in previous screen (to return to later) + @Nullable + UnaryOperator create(ModDisplayInfo displayInfo); +} diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 3a4639571a5..54491e2f3e0 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -15,7 +15,6 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Path; import java.time.Month; import java.time.MonthDay; @@ -23,7 +22,6 @@ import java.util.Comparator; import java.util.List; import java.util.Locale; -import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.UnaryOperator; @@ -77,9 +75,7 @@ import net.neoforged.neoforge.client.gui.widget.ResizableTextureImageWidget; import net.neoforged.neoforge.client.gui.widget.SolidColorWidget; import net.neoforged.neoforge.common.NeoForgeMod; -import org.apache.maven.artifact.versioning.ComparableVersion; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.VisibleForTesting; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -198,7 +194,7 @@ public Component description() { return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), configFactory, versionCheck); } - public ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory, VersionCheckResultSupplier versionCheck) { + private ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory, VersionCheckResultSupplier versionCheck) { super(translatable("neoforge.screen.mods.title")); this.mods = mods; this.modsFolder = modsFolder; @@ -333,20 +329,6 @@ private ImageData loadImage(String type, String modId, ImageResource imageResour return new ImageData(sprite, image.getWidth(), image.getHeight()); } - @VisibleForTesting - @FunctionalInterface - public interface ConfigurationScreenFactory { - // unary operator takes in previous screen (to return to later) - @Nullable - UnaryOperator create(ModDisplayInfo displayInfo); - } - - @VisibleForTesting - @FunctionalInterface - public interface VersionCheckResultSupplier { - VersionChecker.@Nullable CheckResult get(String modId); - } - private class ModsList extends ObjectSelectionList { public ModsList(int height) { // minecraft, width, height, y, itemHeight @@ -757,88 +739,6 @@ public void close() { } } - private static class ChangelogScreen extends Screen { - private final @Nullable Screen lastScreen; - private final ModDisplayInfo info; - private final VersionChecker.CheckResult checkResult; - - @Nullable - private HeaderAndFooterLayout layout; - - protected ChangelogScreen(@Nullable Screen lastScreen, ModDisplayInfo info, VersionChecker.CheckResult checkResult) { - super(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName())); - this.lastScreen = lastScreen; - this.info = info; - this.checkResult = checkResult; - } - - @Override - protected void init() { - super.init(); - - this.layout = new HeaderAndFooterLayout(this); - this.layout.addTitleHeader(Component.translatable("neoforge.screen.mods.changelog.title", info.displayName()), this.font); - - // Footer - final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, Orientation.HORIZONTAL)); - footer.spacing(4).defaultCellSetting().paddingTop(5); - - final Button updateSiteButton = footer.addChild(Button.builder(translatable("neoforge.screen.mods._changelog.open_site"), - _ -> clickUrlAction(minecraft, this, URI.create(this.checkResult.url()))).build()); - updateSiteButton.active = false; - if (checkResult.url() != null) { - String rawUrl = checkResult.url(); - try { - new URI(rawUrl); - updateSiteButton.active = true; - } catch (URISyntaxException exception) { - LOGGER.warn("Failed to create update site URI for mod {} update checker: {}", info.id(), rawUrl); - } - } - footer.addChild(Button.builder(CommonComponents.GUI_BACK, _ -> this.onClose()).build()); - - // Contents - final LinearLayout body = new LinearLayout(0, 0, Orientation.VERTICAL).spacing(4); - - if (checkResult.changes().isEmpty()) { - body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.no_changelog").withStyle(ChatFormatting.ITALIC), this.font) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(310) - .build() - .setCentered(false)); - } else { - for (Map.Entry updateEntry : checkResult.changes().entrySet()) { - body.addChild(FocusableTextWidget.builder(Component.translatable("neoforge.screen.mods.changelog.entry", - Component.literal(updateEntry.getKey().toString()).withStyle(ChatFormatting.BOLD), - Component.literal(updateEntry.getValue())), - this.font) - .alwaysShowBorder(false) - .backgroundFill(FocusableTextWidget.BackgroundFill.NEVER) - .maxWidth(310) - .build() - .setCentered(false)); - } - } - - layout.addToContents(new ScrollableLayout(this.minecraft, body, this.layout.getContentHeight())).setMinWidth(310); - - this.layout.visitWidgets(this::addRenderableWidget); - this.repositionElements(); - } - - @Override - protected void repositionElements() { - assert this.layout != null; - this.layout.arrangeElements(); - } - - @Override - public void onClose() { - this.minecraft.setScreen(lastScreen); - } - } - private static final Comparator COMPARATOR_BY_NAME = Comparator.comparing(c -> c.displayInfo.displayName().getString().toLowerCase(Locale.ROOT)); private enum SortType { diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java new file mode 100644 index 00000000000..59afa44e2b1 --- /dev/null +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.gui.modlist; + +import net.neoforged.fml.VersionChecker; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.Nullable; + +@FunctionalInterface +@ApiStatus.Internal +public interface VersionCheckResultSupplier { + VersionChecker.@Nullable CheckResult get(String modId); +} From b73ba3327841e16c75ac7d8b9efd046009a952d5 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 01:21:36 +0800 Subject: [PATCH 34/45] Move default impls of functional interfaces --- .../modlist/ConfigurationScreenFactory.java | 13 +++++++++++++ .../client/gui/modlist/ModListScreen.java | 19 +------------------ .../modlist/VersionCheckResultSupplier.java | 8 ++++++++ 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java index aea86171b33..30a1e57ccdb 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java @@ -7,6 +7,9 @@ import java.util.function.UnaryOperator; import net.minecraft.client.gui.screens.Screen; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; @@ -16,4 +19,14 @@ public interface ConfigurationScreenFactory { // unary operator takes in previous screen (to return to later) @Nullable UnaryOperator create(ModDisplayInfo displayInfo); + + ConfigurationScreenFactory DEFAULT = displayInfo -> { + final ModContainer container = ModList.get().getModContainerById(displayInfo.id()).orElse(null); + if (container == null) return null; + + final IConfigScreenFactory factory = container.getCustomExtension(IConfigScreenFactory.class).orElse(null); + if (factory == null) return null; + + return parentScreen -> factory.createScreen(container, parentScreen); + }; } diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 54491e2f3e0..96a8fbd26be 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -70,7 +70,6 @@ import net.neoforged.fml.loading.FMLConfig; import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.fml.loading.FMLPaths; -import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import net.neoforged.neoforge.client.gui.widget.BackgroundWithPipingWidget; import net.neoforged.neoforge.client.gui.widget.ResizableTextureImageWidget; import net.neoforged.neoforge.client.gui.widget.SolidColorWidget; @@ -175,23 +174,7 @@ public Component description() { mods.add(TestingResources.exercise("ZexerciseE")); } - ConfigurationScreenFactory configFactory = displayInfo -> { - final ModContainer container = ModList.get().getModContainerById(displayInfo.id()).orElse(null); - if (container == null) return null; - - final IConfigScreenFactory factory = container.getCustomExtension(IConfigScreenFactory.class).orElse(null); - if (factory == null) return null; - - return parentScreen -> factory.createScreen(container, parentScreen); - }; - - VersionCheckResultSupplier versionCheck = modId -> ModList.get().getModContainerById(modId) - .map(ModContainer::getModInfo) - .map(VersionChecker::getResult) - .filter(result -> result.status() == VersionChecker.Status.OUTDATED || result.status() == VersionChecker.Status.BETA_OUTDATED) - .orElse(null); - - return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), configFactory, versionCheck); + return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), ConfigurationScreenFactory.DEFAULT, VersionCheckResultSupplier.DEFAULT); } private ModListScreen(ImmutableList mods, Path modsFolder, ConfigurationScreenFactory configFactory, VersionCheckResultSupplier versionCheck) { diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java index 59afa44e2b1..713f468c2c2 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java @@ -5,6 +5,8 @@ package net.neoforged.neoforge.client.gui.modlist; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModList; import net.neoforged.fml.VersionChecker; import org.jetbrains.annotations.ApiStatus; import org.jspecify.annotations.Nullable; @@ -13,4 +15,10 @@ @ApiStatus.Internal public interface VersionCheckResultSupplier { VersionChecker.@Nullable CheckResult get(String modId); + + VersionCheckResultSupplier DEFAULT = modId -> ModList.get().getModContainerById(modId) + .map(ModContainer::getModInfo) + .map(VersionChecker::getResult) + .filter(result -> result.status() == VersionChecker.Status.OUTDATED || result.status() == VersionChecker.Status.BETA_OUTDATED) + .orElse(null); } From d98e39f02288724e467016a0e83957d378975199 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 01:23:16 +0800 Subject: [PATCH 35/45] Remove old FML translations Replaced by new translations under the neoforge namespace. --- .../resources/assets/neoforge/lang/en_us.json | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 13182664e54..0aaa6d1ef24 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -1,28 +1,5 @@ { "fml.menu.mods": "Mods", - "fml.menu.mods.title": "Mods", - "fml.menu.mods.normal": "Off", - "fml.menu.mods.search": "Search", - "fml.menu.mods.a_to_z": "A-Z", - "fml.menu.mods.z_to_a": "Z-A", - "fml.menu.mods.config": "Config", - "fml.menu.mods.openmodsfolder": "Open mods folder", - "fml.menu.modoptions": "Mod Options...", - "fml.menu.mods.info.version": "Version: %1$s", - "fml.menu.mods.info.idstate": "ModID: %1$s State: {1,lower}", - "fml.menu.mods.info.credits": "Credits: %1$s", - "fml.menu.mods.info.authors": "Authors: %1$s", - "fml.menu.mods.info.displayurl": "Homepage: %1$s", - "fml.menu.mods.info.license": "License: %1$s", - "fml.menu.mods.info.securejardisabled": "Secure mod features disabled, update JDK", - "fml.menu.mods.info.signature": "Signature: %1$s", - "fml.menu.mods.info.signature.unsigned": "UNSIGNED", - "fml.menu.mods.info.trust": "Trust: %1$s", - "fml.menu.mods.info.trust.noauthority": "None", - "fml.menu.mods.info.nochildmods": "No child mods found", - "fml.menu.mods.info.childmods": "Child mods: %1$s", - "fml.menu.mods.info.updateavailable": "Update available: %1$s", - "fml.menu.mods.info.changelogheader": "Changelog:", "fml.menu.multiplayer.compatible": "Compatible FML modded server\n{0,choice,1#1 mod|1<%1$s mods} present", "fml.menu.multiplayer.incompatible": "Incompatible FML modded server", "fml.menu.multiplayer.incompatible.extra": "Incompatible FML modded server\n%1$s", From cd3eb97735a3f667666382d446ab504ee49fa441 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Tue, 28 Apr 2026 01:23:47 +0800 Subject: [PATCH 36/45] Fix inconsistency in translation keys --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 4 ++-- src/main/resources/assets/neoforge/lang/en_us.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 96a8fbd26be..d712c49e9c9 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -654,10 +654,10 @@ public void update() { "neoforge.screen.mods.info.subtitle", Component.literal(displayInfo.id()).withStyle(style -> style .withItalic(true) - .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.modid.click"))) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.info.subtitle.modid.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.id()))), Component.literal(displayInfo.version()).withStyle(style -> style - .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.list.subtitle.version.click"))) + .withHoverEvent(new HoverEvent.ShowText(Component.translatable("neoforge.screen.mods.info.subtitle.version.click"))) .withClickEvent(new ClickEvent.CopyToClipboard(displayInfo.version())))) .withStyle(ChatFormatting.GRAY)); this.idAndVersionWidget.visible = true; diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 0aaa6d1ef24..3fe4c07f2cf 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -37,8 +37,8 @@ "neoforge.screen.mods.info.update": "Update available: %s", "neoforge.screen.mods.info.update.click": "See changelog", "neoforge.screen.mods.info.subtitle": "%s %s", - "neoforge.screen.mods.list.subtitle.modid.click": "Click to copy mod ID", - "neoforge.screen.mods.list.subtitle.version.click": "Click to copy version", + "neoforge.screen.mods.info.subtitle.modid.click": "Click to copy mod ID", + "neoforge.screen.mods.info.subtitle.version.click": "Click to copy version", "neoforge.screen.mods.info.authors": "Authors: %s", "neoforge.screen.mods.info.credits": "Credits: %s", "neoforge.screen.mods.info.license": "License: %s", From 66c6c59d3524f481f23267e105d68c6e534bda7b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 30 Apr 2026 01:52:40 +0800 Subject: [PATCH 37/45] Add support for modUrl as fallback for displayURL --- .../neoforge/client/gui/modlist/DefaultModDisplayInfo.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java index afa83151b73..a1201230830 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java @@ -118,6 +118,7 @@ private ImageResource convertPath(String path) { @Nullable public URI displayUrl() { return container.getModInfo().getConfig().getConfigElement("displayURL") + .or(() -> container.getModInfo().getConfig().getConfigElement("modUrl")) .map(URI::create) .orElse(null); } From 0947aebad83ccf5e9c11a2c7f5be362306519c6f Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Thu, 30 Apr 2026 01:53:16 +0800 Subject: [PATCH 38/45] Add support for mod-level issueTrackerURL --- .../neoforge/client/gui/modlist/DefaultModDisplayInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java index a1201230830..e2b68e0a98b 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java @@ -126,7 +126,8 @@ public URI displayUrl() { @Override @Nullable public URI issuesUrl() { - return container.getModInfo().getOwningFile().getConfig().getConfigElement("issueTrackerURL") + return container.getModInfo().getConfig().getConfigElement("issueTrackerURL") + .or(() -> container.getModInfo().getOwningFile().getConfig().getConfigElement("issueTrackerURL")) .map(URI::create) .orElse(null); } From 9f7052220fc0847da3e2b3d887b9d81b6ef8e182 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 12:23:16 +0800 Subject: [PATCH 39/45] Fix typo for open update site button translation key --- .../neoforged/neoforge/client/gui/modlist/ChangelogScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java index 1eb230d41a6..6110df20884 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ChangelogScreen.java @@ -55,7 +55,7 @@ protected void init() { final LinearLayout footer = layout.addToFooter(new LinearLayout(0, 0, LinearLayout.Orientation.HORIZONTAL)); footer.spacing(4).defaultCellSetting().paddingTop(5); - final Button updateSiteButton = footer.addChild(Button.builder(translatable("neoforge.screen.mods._changelog.open_site"), + final Button updateSiteButton = footer.addChild(Button.builder(translatable("neoforge.screen.mods.changelog.open_site"), _ -> clickUrlAction(minecraft, this, URI.create(this.checkResult.url()))).build()); updateSiteButton.active = false; if (checkResult.url() != null) { From f4f7a48759df488c3dd335297b19335ce0333691 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 13:03:00 +0800 Subject: [PATCH 40/45] Add javadocs --- .../modlist/ConfigurationScreenFactory.java | 3 ++ .../gui/modlist/DefaultModDisplayInfo.java | 28 ++++++++++++++++++- .../client/gui/modlist/ImageResource.java | 18 ++++++++++++ .../client/gui/modlist/ModDisplayInfo.java | 26 +++++++++++++++-- .../client/gui/modlist/ModListScreen.java | 6 ++-- .../modlist/VersionCheckResultSupplier.java | 3 ++ 6 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java index 30a1e57ccdb..ca1992b409d 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ConfigurationScreenFactory.java @@ -11,10 +11,13 @@ import net.neoforged.fml.ModList; import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.VisibleForTesting; import org.jspecify.annotations.Nullable; +/// Configuration screen factory used by the mod list screen. For internal use only. @FunctionalInterface @ApiStatus.Internal +@VisibleForTesting public interface ConfigurationScreenFactory { // unary operator takes in previous screen (to return to later) @Nullable diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java index e2b68e0a98b..9fa3a8cf28f 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/DefaultModDisplayInfo.java @@ -18,9 +18,11 @@ import net.neoforged.fml.i18n.FMLTranslations; import net.neoforged.neoforge.resource.ResourcePackLoader; import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.neoforgespi.locating.IModFile; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; +/// The default implementation of [ModDisplayInfo]. public class DefaultModDisplayInfo implements ModDisplayInfo { private static final Logger LOGGER = LogUtils.getLogger(); @@ -30,21 +32,25 @@ public DefaultModDisplayInfo(ModContainer container) { this.container = container; } + /// {@inheritDoc} @Override public String id() { return container.getModId(); } + /// {@inheritDoc} @Override public Component displayName() { return Component.literal(container.getModInfo().getDisplayName()); } + /// {@inheritDoc} @Override public String version() { return container.getModInfo().getVersion().toString(); } + /// {@inheritDoc} This pulls from the `authors` key of the mod info. @Override public Component authors() { return container.getModInfo().getConfig().getConfigElement("authors") @@ -52,6 +58,7 @@ public Component authors() { .orElseGet(Component::empty); } + /// {@inheritDoc} This pulls from the `credits` key of the mod info. @Override public Component credits() { return container.getModInfo().getConfig().getConfigElement("credits") @@ -59,12 +66,14 @@ public Component credits() { .orElseGet(Component::empty); } + /// {@inheritDoc} This uses the translation key `neoforge.screen.mods.info.description.[modid]` if available, where `[modid]` is the [mod ID][#id()], with a fallback to the `description` key of the mod info. @Override public Component description() { //noinspection UnstableApiUsage return Component.translatable(FMLTranslations.getPattern("neoforge.screen.mods.info.description." + id(), container.getModInfo()::getDescription)); } + /// {@inheritDoc} This uses the `license` key of the mod file info. If the `licenseURL` key of the mod file info is set and is a valid URL, then this component has an [open URL click action][ClickEvent.Action#OPEN_URL] with that URL. @Override public Component license() { MutableComponent licenseText = Component.literal(container.getModInfo().getOwningFile().getLicense()); @@ -81,12 +90,18 @@ public Component license() { return licenseText; } + /// {@inheritDoc} This uses the `logoFile` key of the mod file info or, if not available, the mod info. + /// + /// @see #convertPath(String) @Override @Nullable public ImageResource logo() { return container.getModInfo().getLogoFile().map(this::convertPath).orElse(null); } + /// {@inheritDoc} This uses the `iconFile` key of the mod file info or, if not available, the mod info. + /// + /// @see #convertPath(String) @Override @Nullable public ImageResource icon() { @@ -98,7 +113,16 @@ public ImageResource icon() { return null; } - private ImageResource convertPath(String path) { + /// Converts a given path string into an [ImageResource]. + /// + /// The logic for converting a path is as follows, in order: + /// - If the path contains a pound sign (`#`), it is taken as a [root resource][ImageResource#packRoot(String, String)] with the parts before and after the pound sign taken as the pack ID and the resource path, respectively. + /// - If the path has a colon (`:`), is it taken as a [pack asset][ImageResource#packAsset(Identifier)]. + /// - Otherwise, it is assumed to be a root resource with the mod's resource pack as determined by [ResourcePackLoader#getPackName(IModFile)]. + /// + /// @param path the path to convert + /// @return the image resource + public ImageResource convertPath(String path) { if (path.indexOf('#') > 0) { // Contains a pound sign -- it's a root resource, with parts of "#" String[] split = path.split("#", 2); @@ -114,6 +138,7 @@ private ImageResource convertPath(String path) { } } + /// {@inheritDoc} This uses the `displayURL` or, if not available, the `modUrl` key from the mod info. @Override @Nullable public URI displayUrl() { @@ -123,6 +148,7 @@ public URI displayUrl() { .orElse(null); } + /// {@inheritDoc} This uses the `issueTrackerURL` key of the mod file info or, if not available, the mod info. @Override @Nullable public URI issuesUrl() { diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java index a686b05b57f..5c4acce346d 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ImageResource.java @@ -16,14 +16,32 @@ import org.jspecify.annotations.Nullable; /// An image resource. This is primarily used for [mod display info][ModDisplayInfo]. +/// +/// @see #packRoot(String, String) +/// @see #packAsset(Identifier) public sealed interface ImageResource { + /// Retrieves the image resource contents from the given resource manager. If the resource does not exist, this returns `null`. + /// + /// @param resourceManager the resource manager, which is expected to contain client-side resource packs + /// @return the supplier for the image bytes @Nullable IoSupplier get(ResourceManager resourceManager); + /// Creates an image resource that exists within a specific pack and path. + /// + /// The image resource is always tied to the specific pack ID, and is unaffected by any another pack providing a resource at the same path. + /// + /// @param packId the pack ID + /// @param path the path to the image resource + /// @return an image resource for the given pack and path static ImageResource packRoot(String packId, String path) { return new PackRoot(packId, path); } + /// Creates an image resource for a given location, which is searched among the active packs. + /// + /// The image resource will be searched in the active resource manager, and is thus affected by resource pack ordering, filtering, + /// and overriding of resources. static ImageResource packAsset(Identifier id) { return new PackAsset(id); } diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java index 791fb89fd37..c0304c03193 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModDisplayInfo.java @@ -8,35 +8,57 @@ import java.net.URI; import net.minecraft.network.chat.Component; import net.neoforged.fml.IExtensionPoint; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; import org.jspecify.annotations.Nullable; -// TODO: reconsider if extension point is the best way to do this - /// An extension point for information displayed on the [mod list screen][ModListScreen]. +/// +/// Unless otherwise stated in the documentation, provided components may contain [click actions][net.minecraft.network.chat.ClickEvent] +/// and [hover actions][net.minecraft.network.chat.HoverEvent]. +/// +/// @see IConfigScreenFactory Extension point for custom configuration screens public interface ModDisplayInfo extends IExtensionPoint { + /// {@return the mod ID} This is used to retrieve the mod container, [configuration screen extension point][IConfigScreenFactory], + /// and other relevant information. String id(); + /// {@return the display name} This is always displayed, even if [empty][Component#empty()]. + /// Click actions do not work on this component. Component displayName(); + /// {@return the mod version} This is always displayed, even if [empty][Component#empty()]. String version(); + /// {@return the mod authors} This is displayed if it is not [an empty component][Component#empty()]. Component authors(); + /// {@return the credits} This is displayed if it is not [an empty component][Component#empty()]. Component credits(); + /// {@return the mod authors} This is displayed if it is not [an empty component][Component#empty()]. Component description(); + /// {@return the mod license} This is always displayed, even if [empty][Component#empty()]. Component license(); + /// {@return the logo displayed in the info panel, or `null`} + /// + /// The logo is rendered with its original aspect ratio, bounded by the width of the info panel (ordinarily + /// {@value ModListScreen#INFO_PANEL_WIDTH} pixels) and a maximum height of {@value ModListScreen#LOGO_HEIGHT} pixels. @Nullable ImageResource logo(); // rendered as rectangle + /// {@return the icon displayed in the mod list, or `null`} + /// + /// The icon is rendered as a square with sides of {@value ModListScreen#ICON_SIZE} pixels. @Nullable ImageResource icon(); // rendered as a square + /// {@return the URL for the mod homepage, or `null`} If `null`, the homepage button is disabled. @Nullable URI displayUrl(); + /// {@return the URL for the mod issues page, or `null`} If `null`, the issues page button is disabled. @Nullable URI issuesUrl(); } diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index d712c49e9c9..c6dfbea6aaf 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -86,10 +86,10 @@ public class ModListScreen extends Screen { private static final int SIDEBAR_SORT_BUTTON_WIDTH = 50; private static final int SIDEBAR_CONTROLS_HEIGHT = Button.DEFAULT_HEIGHT; private static final int SIDEBAR_MODS_LIST_WIDTH = 150; - private static final int INFO_PANEL_WIDTH = 250; + static final int INFO_PANEL_WIDTH = 250; private static final int INFO_PANEL_FRAME_PADDING = 2; - private static final int ICON_SIZE = 24; - private static final int LOGO_HEIGHT = 50; + static final int ICON_SIZE = 24; + static final int LOGO_HEIGHT = 50; private final ImmutableList mods; private final Path modsFolder; diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java index 713f468c2c2..b1a040b101f 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/VersionCheckResultSupplier.java @@ -9,10 +9,13 @@ import net.neoforged.fml.ModList; import net.neoforged.fml.VersionChecker; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.VisibleForTesting; import org.jspecify.annotations.Nullable; +/// Version check result supplier used by the mod list screen. For internal use only. @FunctionalInterface @ApiStatus.Internal +@VisibleForTesting public interface VersionCheckResultSupplier { VersionChecker.@Nullable CheckResult get(String modId); From e65563f9149ca9c7f4d5b57e2beb501063d99a80 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 13:04:29 +0800 Subject: [PATCH 41/45] Remove testing resources --- .../client/gui/modlist/ModListScreen.java | 11 - .../client/gui/modlist/TestingResources.java | 230 ------------------ 2 files changed, 241 deletions(-) delete mode 100644 src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index c6dfbea6aaf..3a9ec792de0 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -163,17 +163,6 @@ public Component description() { mods.add(displayInfo); } - // TODO: remove before publish - if (!FMLEnvironment.isProduction()) { - mods.add(TestingResources.neoPride()); - mods.add(TestingResources.winterFox()); - mods.add(TestingResources.exercise("ZexerciseA")); - mods.add(TestingResources.exercise("ZexerciseB")); - mods.add(TestingResources.exercise("ZexerciseC")); - mods.add(TestingResources.exercise("ZexerciseD")); - mods.add(TestingResources.exercise("ZexerciseE")); - } - return new ModListScreen(mods.build(), FMLPaths.MODSDIR.get(), ConfigurationScreenFactory.DEFAULT, VersionCheckResultSupplier.DEFAULT); } diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java deleted file mode 100644 index 2c5cacefbe7..00000000000 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/TestingResources.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.neoforge.client.gui.modlist; - -import java.net.URI; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.ClickEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.MutableComponent; -import net.minecraft.network.chat.TextColor; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.Nullable; - -// TODO: delete once testing is finished -@ApiStatus.Internal -class TestingResources { - static ModDisplayInfo winterFox() { - return new ModDisplayInfo() { - @Override - public String id() { - return "winterfox"; - } - - @Override - public Component displayName() { - //noinspection UnnecessaryUnicodeEscape - return Component.literal("\u2744 Winter Fox \u2744"); - } - - @Override - public String version() { - return "2024.12"; - } - - @Override - public Component authors() { - return Component.empty(); - } - - @Override - public Component credits() { - return Component.empty(); - } - - @Override - public Component description() { - return Component.translatable("%s \n %s", - Component.literal("test ".repeat(20)), - Component.literal("Click this link!").withStyle(style -> style - .applyFormat(ChatFormatting.UNDERLINE) - .withClickEvent(new ClickEvent.OpenUrl(URI.create("https://neoforged.net"))) - .withHoverEvent(new HoverEvent.ShowText(Component.literal("boop!"))))); - } - - @Override - public Component license() { - return Component.literal("BSD-0").withStyle(style -> style.withHoverEvent(new HoverEvent.ShowText(Component.literal("PLS WORK.")))); - } - - @Override - public ImageResource logo() { - return ImageResource.packRoot("mod/neoforge", "snowy_boi.png"); - } - - @Override - public ImageResource icon() { - return ImageResource.packRoot("mod/neoforge", "snowy_boi.png"); - } - - @Nullable - @Override - public URI displayUrl() { - return null; - } - - @Nullable - @Override - public URI issuesUrl() { - return null; - } - }; - } - - static ModDisplayInfo neoPride() { - return new ModDisplayInfo() { - @Override - public String id() { - return "neo_pride"; - } - - @Override - public Component displayName() { - final MutableComponent base = Component.empty(); - TextColor[] colors = { - TextColor.fromRgb(0xaa212b), // red - TextColor.fromRgb(0xfb8918), // orange - TextColor.fromRgb(0xffe359), // yellow - TextColor.fromRgb(0x32d850), // green - TextColor.fromRgb(0x3894ff), // blue - TextColor.fromRgb(0x6e5cb8) // violet - }; - int index = 0; - for (char c : "NeoForged Pride".toCharArray()) { - if (c != ' ') { - TextColor color = colors[index++ % colors.length]; - base.append(Component.literal(String.valueOf(c)).withStyle(s -> s.withColor(color))); - } else { - base.append(Component.literal(" ")); - } - } - return base; - } - - @Override - public String version() { - return "2024.06"; - } - - @Override - public Component authors() { - return Component.empty(); - } - - @Override - public Component credits() { - return Component.empty(); - } - - @Override - public Component description() { - return Component.empty(); - } - - @Override - public Component license() { - return Component.literal("No rights reserved!"); - } - - @Nullable - @Override - public ImageResource logo() { - return null; - } - - @Override - public ImageResource icon() { - return ImageResource.packRoot("mod/neoforge", "neoforged_pride.png"); - } - - @Nullable - @Override - public URI displayUrl() { - return null; - } - - @Nullable - @Override - public URI issuesUrl() { - return null; - } - }; - } - - static ModDisplayInfo exercise(String modId) { - return new ModDisplayInfo() { - @Override - public String id() { - return modId; - } - - @Override - public Component displayName() { - return Component.literal(modId + " " + "HA".repeat(10)); - } - - @Override - public String version() { - return "01.02.03.04.05.06.07.08.09"; - } - - @Override - public Component authors() { - return Component.empty(); - } - - @Override - public Component credits() { - return Component.empty(); - } - - @Override - public Component description() { - return Component.empty(); - } - - @Override - public Component license() { - return Component.literal("Some rights reserved!"); - } - - @Nullable - @Override - public ImageResource logo() { - return null; - } - - @Nullable - @Override - public ImageResource icon() { - return null; - } - - @Nullable - @Override - public URI displayUrl() { - return null; - } - - @Nullable - @Override - public URI issuesUrl() { - return null; - } - }; - } -} From a56dc1b015047bcac75fb487573fbacbff42b565 Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 13:06:27 +0800 Subject: [PATCH 42/45] Remove default sort translation key --- src/main/resources/assets/neoforge/lang/en_us.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 3fe4c07f2cf..4b1c3616b90 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -27,7 +27,6 @@ "neoforge.screen.mods.button.open_folder": "Open Mods Folder", "neoforge.screen.mods.button.sort": "Sort", - "neoforge.screen.mods.sort.default": "Default", "neoforge.screen.mods.sort.a_to_z": "A-Z", "neoforge.screen.mods.sort.a_to_z.hover": "In alphabetical order by display name.", "neoforge.screen.mods.sort.z_to_a": "Z-A", From 9d04eab55184594214a2357e44db1f35f0066b9e Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 13:08:23 +0800 Subject: [PATCH 43/45] Cleanup translation keys --- .../neoforge/client/gui/modlist/ModListScreen.java | 2 +- src/main/resources/assets/neoforge/lang/en_us.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index 3a9ec792de0..cf40c8ca727 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -469,7 +469,7 @@ public ModInfoPanel(int width, int height) { defaultHandleClickEvent(clickEvent, ModListScreen.this.minecraft, ModListScreen.this); } }); - this.newerVersionButton = contentLayout.addChild(Button.builder(translatable("Open update changelog"), + this.newerVersionButton = contentLayout.addChild(Button.builder(translatable("neoforge.screen.mods.button.changelog"), _ -> { if (this.selected != null && this.selected.checkResult != null) { this.openChangelogScreen(this.selected.checkResult); diff --git a/src/main/resources/assets/neoforge/lang/en_us.json b/src/main/resources/assets/neoforge/lang/en_us.json index 4b1c3616b90..a7f2b42c28f 100644 --- a/src/main/resources/assets/neoforge/lang/en_us.json +++ b/src/main/resources/assets/neoforge/lang/en_us.json @@ -27,6 +27,10 @@ "neoforge.screen.mods.button.open_folder": "Open Mods Folder", "neoforge.screen.mods.button.sort": "Sort", + "neoforge.screen.mods.button.homepage": "Homepage", + "neoforge.screen.mods.button.issues": "Issues", + "neoforge.screen.mods.button.config": "Config", + "neoforge.screen.mods.button.changelog": "See update changelog", "neoforge.screen.mods.sort.a_to_z": "A-Z", "neoforge.screen.mods.sort.a_to_z.hover": "In alphabetical order by display name.", "neoforge.screen.mods.sort.z_to_a": "Z-A", @@ -34,16 +38,12 @@ "neoforge.screen.mods.search": "Search", "neoforge.screen.mods.list.narration": "Mod %s with version %s", "neoforge.screen.mods.info.update": "Update available: %s", - "neoforge.screen.mods.info.update.click": "See changelog", "neoforge.screen.mods.info.subtitle": "%s %s", "neoforge.screen.mods.info.subtitle.modid.click": "Click to copy mod ID", "neoforge.screen.mods.info.subtitle.version.click": "Click to copy version", "neoforge.screen.mods.info.authors": "Authors: %s", "neoforge.screen.mods.info.credits": "Credits: %s", "neoforge.screen.mods.info.license": "License: %s", - "neoforge.screen.mods.button.homepage": "Homepage", - "neoforge.screen.mods.button.issues": "Issues", - "neoforge.screen.mods.button.config": "Config", "neoforge.screen.mods.changelog.title": "Update changelog for %s", "neoforge.screen.mods.changelog.open_site": "Open update site", "neoforge.screen.mods.changelog.no_changelog": "No changelog provided.", From 47fa74a9b90b471adb9349a49f6b28781efb8d5b Mon Sep 17 00:00:00 2001 From: sciwhiz12 Date: Sat, 16 May 2026 13:12:00 +0800 Subject: [PATCH 44/45] Remove other testing resources --- src/main/resources/neoforged_pride.png | Bin 1411 -> 0 bytes src/main/resources/snowy_boi.png | Bin 1205 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/resources/neoforged_pride.png delete mode 100644 src/main/resources/snowy_boi.png diff --git a/src/main/resources/neoforged_pride.png b/src/main/resources/neoforged_pride.png deleted file mode 100644 index 74cc0f71a4142d21d069289d64df0127d69cbb88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1411 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU~J8Fb`J1#c2+1T%1_J8No8Qr zm{>c}*5h!1NUMMF(xo9XZxoayHdx(sb(LA#<+>r@7S~#<8GF9)&oWWb)az@?K6r5c z(N)deo7eHJYf|{Z{OHYtB`=gz?@bqpI#TiQqusgP&p+I&ZMd+;=<1u9%pqruE_Lph zae94MV>ru0XO6`Z6KAYcH|~zCxOUz4YtY@J71PS^&%e4{#E0#N%OlHW(qbRkKAu^& zwD_KS--b;~1m+9-OO!Z1a$k2^Z-1elrOt;l9TR=e3%hq54H7>cd*)P|myNGXXh`p; z);XuSp6i~=+IsHR_b2*1;#;QjT+ifHZd6s7*u)`O8WPajD)ClIQ|(fT-1iUsmj6U{ zrWA!9II*!oD#W8t{ttiM`D;3UzPmghT3q;@bXz{zAwfLDH0Ss48{4AQ8~#h~xMcM& zN_fU!mAw})E^>GHZ&pxv_u>=V2Sw}>HJ4PKi&X4&7e=t$JO5(#rvnFc1)_?dU$Z;@ zl6k_?NZtDk?Cl3CIyYUny(jx--tGOiZ-4$}nE0`1^8&Zo6M^x_g3(ARg&i_fAC4 zQxoww>$=&uYr%}Yv%NYa*Vra!Gf!5$9P?|cq433rP4}P7S|}jOAOMH|wl!wm&@ivw z-*&wC-m}WB;m2=3PP%sYs%jcP`}Uh*%MTfzefsZhOVQt7n%sBQf*5~rFtEVkKK6*r z_3H~WPKJJ79$aKL(`WWE-Z1^!&t?_+7GK=9Z11}HTnrp=`17dw{|5_x{G0KWecQ7? zE#Dvg;#Xg*8@-^)^UNxy*FMR0^J7Ie-}GtCJQI?(cGh;)9D9$_eP=o@_HRFp?B5fQ z^uKWKy=Aj@uC|(2>O5pA4F_q9f|q-D)^dI2>AHFSO3RGe-NlDY!gG)NCm&w9J87F? zgueWZ&G+||oekd>%*X(T2}^Smrb-JQzRR6?dxiMZcF*!Yh*y-2)ZaP1mD zn}Wh4WrsG``eyAj-?L+t_@ARQ{_UDITi4mr{`sNTnp*HAqQH>yQT%qx8td}kc|09A zr~TTHdFIQJ88d9{ulumVV+s^!``AAm$liT9d-sh)FE$^yl)wCXzWti({z!@%{yfUg z-Kl0LdM9A>>J&W{tNXDRKC5c$Pd{_3XZQLQz_JkrCmh`$rMG0tEhA)y{IRczTJcuI o>E(wN>o2<_nG4K~3>xYG7(OPQE?E|T`89~|>FVdQ&MBb@09b@$D*ylh diff --git a/src/main/resources/snowy_boi.png b/src/main/resources/snowy_boi.png deleted file mode 100644 index 197a8649600c23e6ded1667d695836fa50fc4cf7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1205 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9GG!XV7ZFl&wk0|QIC zr;B4q#hkZu{XIkqMUJPZ9CW_8%9Z^y|I33X6nK<5mv+l>Gc9y}vef?ji$#v=iu$hY zd5wW(Tvn~yw@$w18Np;I>Qkbta;c1)Kkm-Doj>oHJy9MaZ{h1_j``<6$xHAngY+2bePn>q1ce?lVK@P80S;J+u&!o=4>=$8> z;bP!pWpLMJejazGW8*RIvq?VMYquEbbA0*qD#C1ap0;8A$F|Jf3qnlRy6T2E&we!} zYG#J+SCbh?z5tuj7sWR3<$KZO;^3lAiK`!#UoI}?GW}Avfw?d3^_kmx>(yKBP8>}*3J~i&D%ht7*Z}DME3^I z#?~vBFI<}>U$1v;k)DLD&CigYlhey?C1OO^O#j!chBqqr9W;6I>Vd7{HPMT_SFGx+ zkH#Wvl78qPBb&xXOE0yFR=6*I{kLFs&2618*P4@TIy;UY78DUHgjAx=*cN ziK&K6N|FI4ph=%{!E}Yt}vef&j;nlB(7r+1TtTW#6PXn{HNk zJ^1IZMicSP{5CAm%vfR1zhR?^`0KAzkH0YAe0k+L@$x+f-|cgGe7yEQ$TjN1ERGti z#hbYwU3Te^58AaN><%b?S`Pc>M%fgybwsmI-uoprxBvax2FZpu{IKNnllPo2 Date: Sun, 17 May 2026 04:48:39 +0800 Subject: [PATCH 45/45] Select a single entry if only one left --- .../neoforged/neoforge/client/gui/modlist/ModListScreen.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java index cf40c8ca727..d9f6101e6f2 100644 --- a/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java +++ b/src/client/java/net/neoforged/neoforge/client/gui/modlist/ModListScreen.java @@ -269,9 +269,12 @@ private void updateModsList() { } final ModsList.Entry selected = this.displayList.getSelected(); this.displayList.replaceEntries(entries); - // Reselect the previously-selected if possible if (entries.contains(selected)) { + // Reselect the previously-selected if possible this.displayList.setSelected(selected); + } else if (entries.size() == 1) { + // If there is only one entry, select that + this.displayList.setSelected(entries.getFirst()); } }