diff --git a/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java b/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java index b1460f80a..9f2f0529d 100644 --- a/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java +++ b/src/main/java/net/coderbot/iris/gui/element/ShaderPackOptionList.java @@ -11,6 +11,7 @@ import net.coderbot.iris.gui.screen.ShaderPackScreen; import net.coderbot.iris.shaderpack.ShaderPack; import net.coderbot.iris.shaderpack.option.menu.OptionMenuContainer; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.renderer.Tessellator; import java.util.ArrayList; @@ -24,6 +25,12 @@ public class ShaderPackOptionList extends IrisGuiSlot { private OptionMenuContainer container; private final List entries = new ArrayList<>(); + @Getter + private boolean searchModeActive = false; + private String typedSearchQuery = ""; + private int reservedLeftWidth = 48; // Pixels reserved on the left of the header row for the search/clear button + gap. + private GuiTextField searchBox = null; + public ShaderPackOptionList(ShaderPackScreen screen, NavigationController navigation, ShaderPack pack, Minecraft client, int width, int height, int top, int bottom, int left, int right) { super(client, width, height, top, bottom, 24); this.navigation = navigation; @@ -36,6 +43,74 @@ public class ShaderPackOptionList extends IrisGuiSlot { this.container = pack.getMenuContainer(); } + + /** Applies {@code query} to the option container and rebuilds the list rows. */ + public void updateSearchQuery(String query) { + typedSearchQuery = query != null ? query : ""; + if (this.container != null) { + this.container.setSearchQuery(query); + } + this.rebuild(); + } + + /** Resets all search state and clears the active filter without rebuilding entries. */ + public void disableSearchMode() { + searchModeActive = false; + typedSearchQuery = ""; + searchBox = null; + if (this.container != null) { + this.container.setSearchQuery(null); + } + } + + public void disableSearchModeAndRebuild() { + disableSearchMode(); + this.rebuild(); + } + + public void enableSearchModeAndRebuild() { + searchModeActive = true; + this.rebuild(); // First rebuild so that the button width is the current one + int listWidth = getListWidth(); + int entryX = this.width / 2 - listWidth / 2; + int boxX = entryX + reservedLeftWidth + 3; + int boxY = this.top + 5; // got figured out by trial and error and works, don't question + int boxW = Math.max(40, listWidth - reservedLeftWidth - 5); + this.searchBox = new GuiTextField(Minecraft.getMinecraft().fontRenderer, boxX, boxY, boxW, 14); + this.searchBox.setMaxStringLength(64); + this.searchBox.setFocused(true); + } + + /** True when the navigation stack is non-empty (a sub-screen is open). */ + public boolean isOnSubScreen() { + return navigation != null && navigation.getCurrentScreen() != null; + } + + public void setReservedLeftWidth(int w) { + reservedLeftWidth = Math.max(0, w); + } + + /** + * Routes a keystroke into the live search text field, then propagates any text change + * to the option container and rebuilds the filtered list. + */ + public void handleSearchKeyTyped(char typedChar, int keyCode) { + if (searchBox == null || !searchModeActive) return; + searchBox.textboxKeyTyped(typedChar, keyCode); + String newText = searchBox.getText(); + if (!newText.equals(typedSearchQuery)) { + updateSearchQuery(newText); + } + } + + /** Draws the search text box overlay above the header row */ + public void drawSearchBox() { + if (searchBox == null || !searchModeActive) return; + // Hide when the header has scrolled out of view + if (this.amountScrolled > 0.5f) return; + searchBox.drawTextBox(); + } + public void rebuild() { this.entries.clear(); this.amountScrolled = 0; @@ -56,7 +131,7 @@ public class ShaderPackOptionList extends IrisGuiSlot { } public void addHeader(String text, boolean backButton) { - this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton)); + this.addEntry(new HeaderEntry(this.screen, this.navigation, text, backButton, this)); } public void addWidgets(int columns, List> elements) { @@ -86,6 +161,15 @@ public class ShaderPackOptionList extends IrisGuiSlot { return entries.size(); } + @Override + public boolean mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Pass the click to the search box first so it can update its focus state + if (searchModeActive && searchBox != null && amountScrolled <= 0.5f) { + searchBox.mouseClicked(mouseX, mouseY, mouseButton); + } + return super.mouseClicked(mouseX, mouseY, mouseButton); + } + @Override protected boolean elementClicked(int index, boolean doubleClick, int mouseX, int mouseY, int mouseButton) { final BaseEntry entry = this.entries.get(index); diff --git a/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java b/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java index 31e169570..34b8e57cd 100644 --- a/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java +++ b/src/main/java/net/coderbot/iris/gui/element/ShaderPackSelectionList.java @@ -14,6 +14,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.resources.I18n; import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.MathHelper; import java.io.IOException; import java.nio.file.FileSystems; @@ -142,10 +143,15 @@ public class ShaderPackSelectionList extends IrisGuiSlot { topButtonRow.allowEnableShadersButton = !names.isEmpty(); int index = 0; + String selectedName = Iris.getIrisConfig().getShaderPackName().orElse(null); + int targetSelectionIndex = -1; for (String name : names) { + this.addPackEntry(index, name); + if (name.equals(selectedName)) { + targetSelectionIndex = index; + } index++; - addPackEntry(index, name); } // Try not to gaslight users in case they swap back and forth from lwjgl2 and lwjgl3 @@ -153,6 +159,29 @@ public class ShaderPackSelectionList extends IrisGuiSlot { ? "pack.iris.list.label" : "pack.iris.list.label.lwjgl2"; addLabelEntries(EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format(footerKey)); + + if (targetSelectionIndex != -1) { + scrollToIndex(targetSelectionIndex); + } + } + + /** + * Scrolls the list to center the entry at {@code targetIndex} in the viewport. + */ + public void scrollToIndex(int targetIndex) { + if (targetIndex < 0 || targetIndex >= this.getSize()) { + return; + } + + this.elementClicked(targetIndex, false, 0, 0); + this.selectedElement = targetIndex; + + int viewportCenterY = (this.bottom - this.top) / 2; + int itemCenterPos = (targetIndex * this.slotHeight) + this.headerPadding + (this.slotHeight / 2); + float targetScroll = (float) (itemCenterPos - viewportCenterY); + float maxScroll = (float) this.func_148135_f(); + + this.amountScrolled = MathHelper.clamp_float(targetScroll, 0.0F, maxScroll); } public void addPackEntry(int index, String name) { diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java index 6769d0f3b..523f16761 100644 --- a/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java +++ b/src/main/java/net/coderbot/iris/gui/element/shaderoptions/HeaderEntry.java @@ -1,9 +1,11 @@ package net.coderbot.iris.gui.element.shaderoptions; +import com.gtnewhorizons.angelica.glsm.GLStateManager; import net.coderbot.iris.Iris; import net.coderbot.iris.gui.GuiUtil; import net.coderbot.iris.gui.NavigationController; import net.coderbot.iris.gui.element.IrisElementRow; +import net.coderbot.iris.gui.element.ShaderPackOptionList; import net.coderbot.iris.gui.screen.ShaderPackScreen; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; @@ -16,58 +18,83 @@ import org.jetbrains.annotations.Nullable; public class HeaderEntry extends BaseEntry { - public static final String BACK_BUTTON_TEXT = EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back"); - public static final String RESET_BUTTON_TEXT_INACTIVE = EnumChatFormatting.GRAY + I18n.format("options.iris.reset"); - public static final String RESET_BUTTON_TEXT_ACTIVE = EnumChatFormatting.YELLOW + I18n.format("options.iris.reset"); - - public static final String RESET_HOLD_SHIFT_TOOLTIP = EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift"); - public static final String RESET_TOOLTIP = EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip"); - public static final String IMPORT_TOOLTIP = I18n.format("options.iris.importSettings.tooltip"); - // .withStyle(style -> style.withColor(TextColor.fromRgb(0x4da6ff))); - public static final String EXPORT_TOOLTIP = I18n.format("options.iris.exportSettings.tooltip"); - // .withStyle(style -> style.withColor(TextColor.fromRgb(0xfc7d3d))); + private static String backButtonText() { return EnumChatFormatting.ITALIC + "< " + I18n.format("options.iris.back"); } + private static String resetButtonInactive() { return EnumChatFormatting.GRAY + I18n.format("options.iris.reset"); } + private static String resetButtonActive() { return EnumChatFormatting.YELLOW + I18n.format("options.iris.reset"); } + private static String resetHoldShiftTooltip() { return EnumChatFormatting.BOLD + I18n.format("options.iris.reset.tooltip.holdShift"); } + private static String resetTooltip() { return EnumChatFormatting.RED + I18n.format("options.iris.reset.tooltip"); } + private static String importTooltip() { return I18n.format("options.iris.importSettings.tooltip"); } + private static String exportTooltip() { return I18n.format("options.iris.exportSettings.tooltip"); } + private static String searchButtonText() { return I18n.format("options.iris.search.button"); } + private static String clearButtonText() { return I18n.format("options.iris.clear.button"); } + private static String searchTooltip() { return I18n.format("options.iris.search.tooltip"); } + private static String clearTooltip() { return I18n.format("options.iris.clear.tooltip"); } private static final int MIN_SIDE_BUTTON_WIDTH = 42; private static final int BUTTON_HEIGHT = 16; + private static final int SEARCH_BOX_GAP = 3; private final ShaderPackScreen screen; + private final ShaderPackOptionList optionList; private final @Nullable IrisElementRow backButton; + /** Width of the left-side button, used to clip the title text away from it. */ + private final int leftButtonWidth; private final IrisElementRow utilityButtons = new IrisElementRow(); private final IrisElementRow.TextButtonElement resetButton; private final IrisElementRow.IconButtonElement importButton; private final IrisElementRow.IconButtonElement exportButton; private final String text; - public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton) { + /** Non-null only on the main-screen header (search/clear toggle). */ + private final @Nullable IrisElementRow.TextButtonElement searchToggleButton; + + public HeaderEntry(ShaderPackScreen screen, NavigationController navigation, String text, boolean hasBackButton, ShaderPackOptionList optionList) { super(navigation); + this.optionList = optionList; + this.screen = screen; + this.text = text; + + IrisElementRow.TextButtonElement toggleRef = null; + if (hasBackButton) { - this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(BACK_BUTTON_TEXT, this::backButtonClicked), - Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(BACK_BUTTON_TEXT) + 8)); + // Sub-screen: navigating here while search is active → force-disable so the search box disappears + if (optionList.isSearchModeActive()) { + optionList.disableSearchMode(); + } + String backText = backButtonText(); + int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(backText) + 8); + this.backButton = new IrisElementRow().add(new IrisElementRow.TextButtonElement(backText, this::backButtonClicked), w); + this.leftButtonWidth = w; } else { - this.backButton = null; + // Main screen: left button is the search/clear toggle + String buttonText = optionList.isSearchModeActive() ? clearButtonText() : searchButtonText(); + IrisElementRow.TextButtonElement searchBtn = new IrisElementRow.TextButtonElement(buttonText, this::searchButtonClicked); + int w = Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(buttonText) + 8); + this.backButton = new IrisElementRow().add(searchBtn, w); + toggleRef = searchBtn; + this.leftButtonWidth = w; + optionList.setReservedLeftWidth(w + SEARCH_BOX_GAP); } - this.resetButton = new IrisElementRow.TextButtonElement(RESET_BUTTON_TEXT_INACTIVE, this::resetButtonClicked); + this.searchToggleButton = toggleRef; + + this.resetButton = new IrisElementRow.TextButtonElement(resetButtonInactive(), this::resetButtonClicked); this.importButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.IMPORT, GuiUtil.Icon.IMPORT_COLORED, this::importSettingsButtonClicked); this.exportButton = new IrisElementRow.IconButtonElement(GuiUtil.Icon.EXPORT, GuiUtil.Icon.EXPORT_COLORED, this::exportSettingsButtonClicked); this.utilityButtons.add(this.importButton, 15).add(this.exportButton, 15) - .add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth((RESET_BUTTON_TEXT_INACTIVE) + 8))); - - this.screen = screen; - this.text = text; + .add(this.resetButton, Math.max(MIN_SIDE_BUTTON_WIDTH, Minecraft.getMinecraft().fontRenderer.getStringWidth(resetButtonInactive()) + 8)); } @Override public void drawEntry(ShaderPackScreen screen, int index, int x, int y, int slotWidth, int slotHeight, Tessellator tessellator, int mouseX, int mouseY, boolean isMouseOver) { Gui.drawRect(x - 3, (y + slotHeight) - 3, x + slotWidth, (y + slotHeight) - 2, 0x66BEBEBE); + // Gui.drawRect leaves GL color set to the rect's color; reset to white so button textures aren't tinted. + GLStateManager.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); final int tickDelta = 0; final FontRenderer font = Minecraft.getMinecraft().fontRenderer; - GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5), x + 5, - ((x + slotWidth) - 10) - this.utilityButtons.getWidth(), y + 5, y + 15, 0xFFFFFF); - GuiUtil.bindIrisWidgetsTexture(); // Draw back button if present @@ -75,26 +102,42 @@ public class HeaderEntry extends BaseEntry { backButton.drawScreen(x, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver); } + if (optionList.isSearchModeActive() && searchToggleButton != null) { + if (searchToggleButton.isHovered()) { + ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, clearTooltip(), mouseX, mouseY - 16)); + } + return; + } + + GuiUtil.drawScrollingText(font, text, x + (int) (slotWidth * 0.5), + x + leftButtonWidth + 5, + ((x + slotWidth) - 10) - this.utilityButtons.getWidth(), + y + 5, y + 15, 0xFFFFFF); + final boolean shiftDown = GuiScreen.isShiftKeyDown(); // Set the appearance of the reset button this.resetButton.disabled = !shiftDown; - this.resetButton.text = shiftDown ? RESET_BUTTON_TEXT_ACTIVE : RESET_BUTTON_TEXT_INACTIVE; + this.resetButton.text = shiftDown ? resetButtonActive() : resetButtonInactive(); // Draw the utility buttons this.utilityButtons.renderRightAligned((x + slotWidth) - 3, y, BUTTON_HEIGHT, mouseX, mouseY, tickDelta, isMouseOver); // Draw the reset button's tooltip if (this.resetButton.isHovered()) { - final String tooltip = shiftDown ? RESET_TOOLTIP : RESET_HOLD_SHIFT_TOOLTIP; + final String tooltip = shiftDown ? resetTooltip() : resetHoldShiftTooltip(); queueBottomRightAnchoredTooltip(mouseX, mouseY, font, tooltip); } // Draw the import/export button tooltips if (this.importButton.isHovered()) { - queueBottomRightAnchoredTooltip(mouseX, mouseY, font, IMPORT_TOOLTIP); + queueBottomRightAnchoredTooltip(mouseX, mouseY, font, importTooltip()); } if (this.exportButton.isHovered()) { - queueBottomRightAnchoredTooltip(mouseX, mouseY, font, EXPORT_TOOLTIP); + queueBottomRightAnchoredTooltip(mouseX, mouseY, font, exportTooltip()); + } + // Search-button tooltip + if (searchToggleButton != null && searchToggleButton.isHovered()) { + ShaderPackScreen.TOP_LAYER_RENDER_QUEUE.add(() -> GuiUtil.drawTextPanel(font, searchTooltip(), mouseX, mouseY - 16)); } } @@ -104,6 +147,9 @@ public class HeaderEntry extends BaseEntry { @Override public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (optionList.isSearchModeActive()) { + return backButton != null && backButton.mouseClicked(mouseX, mouseY, button); + } final boolean backButtonResult = backButton != null && backButton.mouseClicked(mouseX, mouseY, button); final boolean utilButtonResult = utilityButtons.mouseClicked(mouseX, mouseY, button); @@ -122,6 +168,16 @@ public class HeaderEntry extends BaseEntry { return true; } + private boolean searchButtonClicked(IrisElementRow.TextButtonElement button) { + GuiUtil.playButtonClickSound(); + if (optionList.isSearchModeActive()) { + optionList.disableSearchModeAndRebuild(); + } else { + optionList.enableSearchModeAndRebuild(); + } + return true; + } + private boolean resetButtonClicked(IrisElementRow.TextButtonElement button) { if (GuiScreen.isShiftKeyDown()) { Iris.resetShaderPackOptionsOnNextReload(); diff --git a/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java b/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java index 8e7faed96..34d355a54 100644 --- a/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java +++ b/src/main/java/net/coderbot/iris/gui/element/shaderselection/TopButtonRowEntry.java @@ -10,9 +10,10 @@ import net.minecraft.client.resources.I18n; import java.util.function.Function; public class TopButtonRowEntry extends BaseEntry { - private static final String NONE_PRESENT_LABEL = I18n.format("options.iris.shaders.nonePresent"); //.withStyle(ChatFormatting.GRAY); - private static final String SHADERS_DISABLED_LABEL = I18n.format("options.iris.shaders.disabled"); - private static final String SHADERS_ENABLED_LABEL = I18n.format("options.iris.shaders.enabled"); + + private static String nonePresentLabel() { return I18n.format("options.iris.shaders.nonePresent"); } + private static String shadersDisabledLabel() { return I18n.format("options.iris.shaders.disabled"); } + private static String shadersEnabledLabel() { return I18n.format("options.iris.shaders.enabled"); } private final ShaderPackSelectionList shaderPackSelectionList; private final IrisElementRow buttons = new IrisElementRow(); @@ -54,7 +55,7 @@ public class TopButtonRowEntry extends BaseEntry { } private String getEnableDisableLabel() { - return this.allowEnableShadersButton ? this.shadersEnabled ? SHADERS_ENABLED_LABEL : SHADERS_DISABLED_LABEL : NONE_PRESENT_LABEL; + return this.allowEnableShadersButton ? this.shadersEnabled ? shadersEnabledLabel() : shadersDisabledLabel() : nonePresentLabel(); } public boolean mouseClicked(double mouseX, double mouseY, int button) { diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java index e90b9f0be..4bb6cad17 100644 --- a/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java +++ b/src/main/java/net/coderbot/iris/gui/element/widget/BaseOptionElementWidget.java @@ -4,16 +4,19 @@ import net.coderbot.iris.gui.GuiUtil; import net.coderbot.iris.gui.NavigationController; import net.coderbot.iris.gui.screen.ShaderPackScreen; import net.coderbot.iris.shaderpack.option.menu.OptionMenuElement; +import net.coderbot.iris.shaderpack.option.menu.OptionMenuOptionElement; +import net.coderbot.iris.shaderpack.option.menu.ShaderSearchEngine; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.resources.I18n; +import net.minecraft.util.EnumChatFormatting; import org.jetbrains.annotations.Nullable; import java.util.Optional; public abstract class BaseOptionElementWidget extends CommentedElementWidget { - protected static final String SET_TO_DEFAULT = I18n.format("options.iris.setToDefault"); + protected static String setToDefault() { return I18n.format("options.iris.setToDefault"); } protected static final String DIVIDER = ": "; protected String unmodifiedLabel; @@ -101,7 +104,7 @@ public abstract class BaseOptionElementWidget exten protected final void tryRenderTooltip(int mouseX, int mouseY, boolean hovered) { if (GuiScreen.isShiftKeyDown()) { - renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered); + renderTooltip(setToDefault(), mouseX, mouseY, hovered); } else if (this.isLabelTrimmed && !this.screen.isDisplayingComment()) { renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered); } @@ -145,13 +148,39 @@ public abstract class BaseOptionElementWidget exten @Override public Optional getCommentBody() { - final String key = getCommentKey(); - if (key == null) { - return Optional.empty(); + Optional base = Optional.ofNullable(getCommentKey()).map(I18n::format); + + if (this.screen == null || !this.screen.isOptionMenuSearchActive()) { + return base; } - final String translated = GuiUtil.translateLenient(key); - // Don't show comment boxes without a translation, otherwise you get something like option.{commentname}.comment - return translated.equals(key) ? Optional.empty() : Optional.of(translated); + + String pathLabel = buildTranslatedPath(); + if (pathLabel == null) { + return base; + } + + String breadcrumb = EnumChatFormatting.BOLD.toString() + EnumChatFormatting.ITALIC + pathLabel + EnumChatFormatting.RESET; + if (base.isPresent()) { + return Optional.of(breadcrumb + ". " + base.get()); + } + return Optional.of(breadcrumb); + } + + @Nullable + private String buildTranslatedPath() { + if (!(this.element instanceof OptionMenuOptionElement)) return null; + OptionMenuOptionElement optEl = (OptionMenuOptionElement) this.element; + String rawPath = optEl.container.getOptionPath(optEl.optionId); + String[] segments = rawPath.split("/"); + StringBuilder display = new StringBuilder(); + for (String segment : segments) { + if ("root".equals(segment)) continue; + String translated = ShaderSearchEngine.getDisplaySettingsName(segment).replaceAll("\\s+>", ""); + String label = translated.isEmpty() ? segment : translated; + if (display.length() > 0) display.append(" > "); + display.append(label); + } + return display.length() > 0 ? display.toString() : null; } @Override diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java index 9388e7bdb..5765cc8fb 100644 --- a/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java +++ b/src/main/java/net/coderbot/iris/gui/element/widget/BooleanElementWidget.java @@ -9,10 +9,9 @@ import net.coderbot.iris.shaderpack.option.menu.OptionMenuBooleanOptionElement; import net.minecraft.client.resources.I18n; public class BooleanElementWidget extends BaseOptionElementWidget { - private static final String TEXT_TRUE = I18n.format("label.iris.true"); - private static final String TEXT_FALSE = I18n.format("label.iris.false"); - private static final String TEXT_TRUE_DEFAULT = I18n.format("label.iris.true"); - private static final String TEXT_FALSE_DEFAULT = I18n.format("label.iris.false"); + + private static String textTrue() { return I18n.format("label.iris.true"); } + private static String textFalse() { return I18n.format("label.iris.false"); } private final BooleanOption option; @@ -63,11 +62,7 @@ public class BooleanElementWidget extends BaseOptionElementWidget { - private static final String PROFILE_LABEL = I18n.format("options.iris.profile"); - private static final String PROFILE_CUSTOM = I18n.format("options.iris.profile.custom"); + + private static String profileLabel() { return I18n.format("options.iris.profile"); } + private static String profileCustom() { return I18n.format("options.iris.profile.custom"); } private Profile next; private Profile previous; @@ -30,7 +31,7 @@ public class ProfileElementWidget extends BaseOptionElementWidget profileName = result.current.map(p -> p.name); String translationKey = "profile" + (secondProfileSet ? "2." : ".") + profileName.orElse("custom"); - this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(PROFILE_CUSTOM); + this.profileLabel = profileName.map(name -> GuiUtil.translateOrDefault(name, translationKey)).orElse(profileCustom()); } @Override public void drawScreen(int x, int y, int width, int height, int mouseX, int mouseY, float tickDelta, boolean hovered) { - this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(PROFILE_LABEL) + 16)); + this.updateRenderParams(width, width - (Minecraft.getMinecraft().fontRenderer.getStringWidth(profileLabel()) + 16)); this.renderOptionWithValue(x, y, width, height, hovered); } @@ -61,12 +62,12 @@ public class ProfileElementWidget extends BaseOptionElementWidget getCommentTitle() { - return Optional.of(PROFILE_LABEL); + return Optional.of(profileLabel()); } @Override diff --git a/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java b/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java index e264b76aa..d20e6f9bc 100644 --- a/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java +++ b/src/main/java/net/coderbot/iris/gui/element/widget/SliderElementWidget.java @@ -28,7 +28,7 @@ public class SliderElementWidget extends StringElementWidget { } if (GuiScreen.isShiftKeyDown()) { - renderTooltip(SET_TO_DEFAULT, mouseX, mouseY, hovered); + renderTooltip(setToDefault(), mouseX, mouseY, hovered); } else if (!this.screen.isDisplayingComment()) { renderTooltip(this.unmodifiedLabel, mouseX, mouseY, hovered); } diff --git a/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java b/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java index 3aee3e59e..88e23ece2 100644 --- a/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java +++ b/src/main/java/net/coderbot/iris/gui/screen/ShaderPackScreen.java @@ -53,8 +53,8 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { */ public static final Set TOP_LAYER_RENDER_QUEUE = new HashSet<>(); - private static final String SELECT_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title"); - private static final String CONFIGURE_TITLE = EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title"); + private static String selectTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.select.title"); } + private static String configureTitle() { return EnumChatFormatting.GRAY.toString() + EnumChatFormatting.ITALIC + I18n.format("pack.iris.configure.title"); } private static final int COMMENT_PANEL_WIDTH = 314; private final GuiScreen parent; @@ -104,6 +104,11 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { refreshForChangedPack(); } + /** Returns true when the option menu is open and the user has an active search query. */ + public boolean isOptionMenuSearchActive() { + return this.optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive(); + } + @Override public void drawScreen(int mouseX, int mouseY, float delta) { GLStateManager.glPushAttrib(GL11.GL_COLOR_BUFFER_BIT); @@ -124,6 +129,7 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { if (!this.guiHidden) { if (optionMenuOpen && this.shaderOptionList != null) { this.shaderOptionList.drawScreen(mouseX, mouseY, delta); + this.shaderOptionList.drawSearchBox(); } else { this.shaderPackList.drawScreen(mouseX, mouseY, delta); } @@ -144,9 +150,9 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { drawCenteredString(this.fontRendererObj, notificationDialog, (int) (this.width * 0.5), 21, 0xFFFFFF); } else { if (optionMenuOpen) { - drawCenteredString(this.fontRendererObj, CONFIGURE_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF); + drawCenteredString(this.fontRendererObj, configureTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF); } else { - drawCenteredString(this.fontRendererObj, SELECT_TITLE, (int) (this.width * 0.5), 21, 0xFFFFFF); + drawCenteredString(this.fontRendererObj, selectTitle(), (int) (this.width * 0.5), 21, 0xFFFFFF); } } @@ -207,6 +213,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { if (Iris.getCurrentPack().isPresent() && this.navigation != null) { final ShaderPack currentPack = Iris.getCurrentPack().get(); + if (currentPack.getMenuContainer() != null) { + currentPack.getMenuContainer().setSearchQuery(null); + } + this.shaderOptionList = new ShaderPackOptionList(this, this.navigation, currentPack, this.mc, this.width, this.height, 32, this.height - 58, 0, this.width); this.navigation.setActiveOptionList(this.shaderOptionList); @@ -364,6 +374,31 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { @Override protected void keyTyped(char typedChar, int keyCode) { + // Route all input to the search box while search mode is active + if (optionMenuOpen && this.shaderOptionList != null && this.shaderOptionList.isSearchModeActive()) { + if (keyCode == Keyboard.KEY_ESCAPE) { + this.shaderOptionList.disableSearchModeAndRebuild(); + return; + } + // Ctrl+F while search is open: toggle it closed + if (keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) { + GuiUtil.playButtonClickSound(); + this.shaderOptionList.disableSearchModeAndRebuild(); + return; + } + this.shaderOptionList.handleSearchKeyTyped(typedChar, keyCode); + return; + } + + // Ctrl+F: open search on the main option screen + if (optionMenuOpen && this.shaderOptionList != null + && keyCode == Keyboard.KEY_F && GuiScreen.isCtrlKeyDown()) { + if (!this.shaderOptionList.isOnSubScreen()) { + GuiUtil.playButtonClickSound(); + this.shaderOptionList.enableSearchModeAndRebuild(); + return; + } + } if (GuiScreen.isCtrlKeyDown() && keyCode == Keyboard.KEY_D) { this.mc.displayGuiScreen(new GuiYesNo((result, id) -> { Iris.setDebug(result); @@ -496,6 +531,10 @@ public class ShaderPackScreen extends GuiScreen implements HudHideable { } public void onClose() { + if (this.shaderOptionList != null) { + this.shaderOptionList.disableSearchModeAndRebuild(); + } + if (!dropChanges) { applyChanges(); } else { diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java index 7f9b21821..ae26d6884 100644 --- a/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/OptionMenuContainer.java @@ -10,9 +10,12 @@ import net.coderbot.iris.shaderpack.option.ShaderPackOptions; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; +import java.util.Set; public class OptionMenuContainer { public final OptionMenuElementScreen mainScreen; @@ -24,6 +27,8 @@ public class OptionMenuContainer { private final Map, Integer> unusedOptionDumpQueue = new HashMap<>(); // Used by screens with "*" element @Getter private final ProfileSet profiles; @Getter private final ProfileSet profiles2; + private final List originalMainElements = new ArrayList<>(); // Saved snapshot of mainScreen.elements before any search filter is applied. + private final Map cachedOptionPaths = new HashMap<>(); // Full "root/SCREEN1/SCREEN2" path for every option ID, built once after construction public OptionMenuContainer(ShaderProperties shaderProperties, ShaderPackOptions shaderPackOptions, ProfileSet profiles) { this.profiles = profiles; @@ -69,6 +74,10 @@ public class OptionMenuContainer { entry.getKey().addAll(entry.getValue(), elementsToInsert); } + + // Save the original layout and build the path cache once the full tree is constructed + this.originalMainElements.addAll(this.mainScreen.elements); + generateAllPaths(); } // Screens will call this when they contain a "*" element, so that the list of @@ -85,4 +94,81 @@ public class OptionMenuContainer { unusedOptions.remove(optionId); } + + private void generateAllPaths() { + cachedOptionPaths.clear(); + traverseScreen(this.mainScreen, "root", new HashSet()); + } + + private void traverseScreen(OptionMenuElementScreen screen, String currentPath, Set visited) { + if (screen == null) return; + for (OptionMenuElement element : screen.elements) { + if (element instanceof OptionMenuOptionElement optEl) { + if (optEl.optionId != null) { + cachedOptionPaths.putIfAbsent(optEl.optionId, currentPath); + } + } else if (element instanceof OptionMenuLinkElement link) { + if (link.targetScreenId != null) { + String targetId = link.targetScreenId; + if (visited.add(targetId)) { + OptionMenuElementScreen next = this.subScreens.get(targetId); + if (next != null) { + traverseScreen(next, currentPath + "/" + targetId, visited); + } + visited.remove(targetId); + } + } + } + } + } + + public String getOptionPath(String optionId) { + if (optionId == null) return "root"; + return cachedOptionPaths.getOrDefault(optionId, "root"); + } + + /** + * Filters and re-orders the main-screen options to match {@code query}, or restores the + * original layout when {@code query} is null or blank. + */ + public void setSearchQuery(String query) { + if (query == null || query.trim().isEmpty()) { + restoreOriginalLayout(); + return; + } + + String normalizedQuery = query.toLowerCase(Locale.ROOT).trim(); + + List flatOptions = ShaderSearchEngine.getAllOptionsFlattened(usedOptionElements); + List scored = new ArrayList<>(); + + for (OptionMenuOptionElement el : flatOptions) { + int score = ShaderSearchEngine.computeMatchTier(el.optionId, normalizedQuery); + if (score > 0) { + scored.add(new ShaderSearchEngine.ScoredOptionElement( + el, + ShaderSearchEngine.getReadableTranslatedName(el.optionId), + ShaderSearchEngine.getReadableDefaultName(el.optionId), + getOptionPath(el.optionId), + score, + normalizedQuery + )); + } + } + + Collections.sort(scored); + applyFilteredLayout(scored); + } + + private void applyFilteredLayout(List sortedElements) { + this.mainScreen.elements.clear(); + for (ShaderSearchEngine.ScoredOptionElement scored : sortedElements) { + this.mainScreen.elements.add(scored.getElement()); + } + } + + private void restoreOriginalLayout() { + this.mainScreen.elements.clear(); + this.mainScreen.elements.addAll(this.originalMainElements); + } } diff --git a/src/main/java/net/coderbot/iris/shaderpack/option/menu/ShaderSearchEngine.java b/src/main/java/net/coderbot/iris/shaderpack/option/menu/ShaderSearchEngine.java new file mode 100644 index 000000000..ea793b7f8 --- /dev/null +++ b/src/main/java/net/coderbot/iris/shaderpack/option/menu/ShaderSearchEngine.java @@ -0,0 +1,251 @@ +package net.coderbot.iris.shaderpack.option.menu; + +import lombok.Getter; +import net.coderbot.iris.Iris; +import net.coderbot.iris.shaderpack.ShaderPack; +import net.minecraft.client.resources.I18n; +import org.jspecify.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; + +/** + * Utility class for searching and scoring shader option elements based on a query string. + *
By SpacEagle17 + */ +public class ShaderSearchEngine { + private static final String WHOLE_WORD_REGEX = "(?<=^|[^a-zA-Z0-9])%s(?=$|[^a-zA-Z0-9])"; + private static final String STARTS_WITH_REGEX = "(?<=^|[^a-zA-Z0-9])%s"; + private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§."); + + /** + * Bitmask of bits from readable-name (translated + default) comparisons. + * Used by the comparator to separate name-match quality from rawId/comment noise. + */ + private static final int READABLE_NAME_BITS = + (1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 3) | (1 << 2); + + + private static String strippedLanguageString(String key) { + String translated = I18n.format(key); + if (translated.equals(key)) return ""; + return COLOR_CODE_PATTERN.matcher(translated).replaceAll(""); + } + + /** Translated display name for a sub-screen ID (looks up {@code "screen."}). */ + public static String getDisplaySettingsName(String screenId) { + return strippedLanguageString("screen." + screenId); + } + + /** Lowercase Minecraft-locale translation of an option's human-readable name. */ + public static String getReadableTranslatedName(String optionId) { + return strippedLanguageString("option." + optionId).toLowerCase(Locale.ROOT); + } + + /** + * Lowercase default (en_us from the shader pack's lang folder) translation. + * Returns {@code ""} when the current pack has no en_us lang file or the key is missing. + */ + public static String getReadableDefaultName(String optionId) { + try { + Optional packOpt = Iris.getCurrentPack(); + if (packOpt.isEmpty()) return ""; + Map translations = packOpt.get().getLanguageMap().getTranslations("en_us"); + if (translations == null) return ""; + String value = translations.get("option." + optionId); + if (value == null) return ""; + return COLOR_CODE_PATTERN.matcher(value).replaceAll("").toLowerCase(Locale.ROOT); + } catch (Throwable t) { + return ""; + } + } + + private static boolean isOnlyAscii(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) > 127) return false; + } + return true; + } + + /** + * Computes a 14-bit bitmask score for how well {@code optionId} matches {@code query}. + * Returns 0 when there is no match at all. + *
Read code to know what each bit means lmao + */ + public static int computeMatchTier(String optionId, String query) { + try { + if (optionId == null || query == null) return 0; + String q = query.toLowerCase(Locale.ROOT).trim(); + if (q.isEmpty()) return 0; + + String translatedName = getReadableTranslatedName(optionId); + String defaultName = getReadableDefaultName(optionId); + + // Single ASCII char: only match when a readable name begins with it + if (q.length() == 1 && isOnlyAscii(q)) { + return (!translatedName.isEmpty() && translatedName.startsWith(q)) ? 1 : 0; + } + + String escaped = Pattern.quote(q); + Pattern wholeWord = Pattern.compile(String.format(WHOLE_WORD_REGEX, escaped)); + Pattern startsWith = Pattern.compile(String.format(STARTS_WITH_REGEX, escaped)); + + String rawId = optionId.toLowerCase(Locale.ROOT); + String commentText = strippedLanguageString("option." + optionId + ".comment").toLowerCase(Locale.ROOT); + + int score = 0; + if (!translatedName.isEmpty() && translatedName.equals(q)) score |= (1 << 13); + if (!defaultName.isEmpty() && defaultName.equals(q)) score |= (1 << 12); + if (!translatedName.isEmpty() && wholeWord.matcher(translatedName).find()) score |= (1 << 11); + if (!defaultName.isEmpty() && wholeWord.matcher(defaultName).find()) score |= (1 << 10); + if (!translatedName.isEmpty() && startsWith.matcher(translatedName).find()) score |= (1 << 9); + if (!defaultName.isEmpty() && startsWith.matcher(defaultName).find()) score |= (1 << 8); + if (wholeWord.matcher(rawId).find()) score |= (1 << 7); + if (!commentText.isEmpty() && wholeWord.matcher(commentText).find()) score |= (1 << 6); + if (startsWith.matcher(rawId).find()) score |= (1 << 5); + if (!commentText.isEmpty() && startsWith.matcher(commentText).find()) score |= (1 << 4); + if (!translatedName.isEmpty() && translatedName.contains(q)) score |= (1 << 3); + if (!defaultName.isEmpty() && defaultName.contains(q)) score |= (1 << 2); + if (rawId.contains(q)) score |= (1 << 1); + if (!commentText.isEmpty() && commentText.contains(q)) score |= 1; + + return score; + } catch (Exception e) { + return 0; + } + } + + /** De-duplicates and flattens the pool of registered option elements. */ + public static List getAllOptionsFlattened(List usedOptionElements) { + List flat = new ArrayList<>(); + List seen = new ArrayList<>(); + for (OptionMenuOptionElement el : usedOptionElements) { + if (el == null) continue; + String id = el.optionId != null ? el.optionId : el.toString(); + if (!seen.contains(id)) { + seen.add(id); + flat.add(el); + } + } + return flat; + } + + /** + * A scored search result carrying the original element and all data required for sorting. + */ + public static final class ScoredOptionElement implements Comparable { + @Getter + private final OptionMenuOptionElement element; + public final String readableTranslatedName; + public final String readableDefaultName; + public final String path; + public final int score; + public final String query; + + public ScoredOptionElement(OptionMenuOptionElement element, + String readableTranslatedName, + String readableDefaultName, + String path, + int score, + String query) { + this.element = element; + this.readableTranslatedName = readableTranslatedName; + this.readableDefaultName = readableDefaultName; + this.path = path; + this.score = score; + this.query = query; + } + + @Override + public int compareTo(@NonNull ScoredOptionElement other) { + String q = this.query != null ? this.query.toLowerCase(Locale.ROOT).trim() : ""; + int r; + if ((r = compareByReadableNameQuality(this, other)) != 0) return r; + if ((r = compareByMatchedWordLength(this, other, q)) != 0) return r; + if ((r = compareByWordCount(this, other)) != 0) return r; + if ((r = compareByFullScore(this, other)) != 0) return r; + if ((r = compareByPrefixBoost(this, other, q)) != 0) return r; + if ((r = compareByPathDepth(this, other)) != 0) return r; + return compareByAlphabetical(this, other); + } + + // 1. How well does the readable name (translated + default) match? + private static int compareByReadableNameQuality(ScoredOptionElement a, ScoredOptionElement b) { + return Integer.compare(b.score & READABLE_NAME_BITS, a.score & READABLE_NAME_BITS); + } + + // 2. Shorter matched word = query covers a higher fraction of that word. + private static int compareByMatchedWordLength(ScoredOptionElement a, ScoredOptionElement b, String q) { + if (q.isEmpty()) return 0; + String aw = findMatchingWord(a.readableTranslatedName, q); + if (aw == null) aw = findMatchingWord(a.readableDefaultName, q); + String bw = findMatchingWord(b.readableTranslatedName, q); + if (bw == null) bw = findMatchingWord(b.readableDefaultName, q); + int aLen = aw != null ? aw.length() : Integer.MAX_VALUE; + int bLen = bw != null ? bw.length() : Integer.MAX_VALUE; + return Integer.compare(aLen, bLen); + } + + // 3. Fewer words = more precise ("Bloom" before "Bloom Strength"). + private static int compareByWordCount(ScoredOptionElement a, ScoredOptionElement b) { + String ae = !a.readableTranslatedName.isEmpty() ? a.readableTranslatedName : a.readableDefaultName; + String be = !b.readableTranslatedName.isEmpty() ? b.readableTranslatedName : b.readableDefaultName; + return Integer.compare(countWords(ae), countWords(be)); + } + + // 4. Higher raw score wins. + private static int compareByFullScore(ScoredOptionElement a, ScoredOptionElement b) { + return Integer.compare(b.score, a.score); + } + + // 5. Prefix boost: readable name starts with the exact query string. + private static int compareByPrefixBoost(ScoredOptionElement a, ScoredOptionElement b, String q) { + if (q.isEmpty()) return 0; + boolean ap = (a.readableTranslatedName != null && a.readableTranslatedName.startsWith(q)) + || (a.readableDefaultName != null && a.readableDefaultName.startsWith(q)); + boolean bp = (b.readableTranslatedName != null && b.readableTranslatedName.startsWith(q)) + || (b.readableDefaultName != null && b.readableDefaultName.startsWith(q)); + if (ap == bp) return 0; + return ap ? -1 : 1; + } + + // 6. Shallower option path wins (fewer slashes = closer to root menu). + private static int compareByPathDepth(ScoredOptionElement a, ScoredOptionElement b) { + return Integer.compare(countSlashes(a.path), countSlashes(b.path)); + } + + // 7. Alphabetical tie-breaker. + private static int compareByAlphabetical(ScoredOptionElement a, ScoredOptionElement b) { + String aId = a.element != null ? a.element.optionId : null; + String bId = b.element != null ? b.element.optionId : null; + if (aId != null && bId != null) return aId.compareTo(bId); + return 0; + } + + private static String findMatchingWord(String name, String query) { + if (name == null || query.isEmpty()) return null; + for (String word : name.split("\\s+")) { + if (word.startsWith(query)) return word; + } + return null; + } + + private static int countWords(String s) { + if (s == null || s.trim().isEmpty()) return 0; + return s.trim().split("\\s+").length; + } + + private static int countSlashes(String path) { + if (path == null) return 0; + int count = 0; + for (int i = 0; i < path.length(); i++) { + if (path.charAt(i) == '/') count++; + } + return count; + } + } +} diff --git a/src/main/resources/assets/angelica/lang/de_DE.lang b/src/main/resources/assets/angelica/lang/de_DE.lang index 1834dd1ff..585e1c9df 100644 --- a/src/main/resources/assets/angelica/lang/de_DE.lang +++ b/src/main/resources/assets/angelica/lang/de_DE.lang @@ -145,3 +145,7 @@ pack.iris.select.title=Auswählen pack.iris.configure.title=Konfigurieren label.iris.true=An label.iris.false=Aus +options.iris.search.button=Suchen +options.iris.clear.button=Schließen +options.iris.search.tooltip=Shader-Einstellungen durchsuchen (Strg + F) +options.iris.clear.tooltip=Shader-Suche beenden (Esc) diff --git a/src/main/resources/assets/angelica/lang/en_US.lang b/src/main/resources/assets/angelica/lang/en_US.lang index 11174b36d..6cd71073d 100644 --- a/src/main/resources/assets/angelica/lang/en_US.lang +++ b/src/main/resources/assets/angelica/lang/en_US.lang @@ -139,6 +139,10 @@ options.iris.shadowDistance.enabled=Allows you to change the maximum distance fo options.iris.shadowDistance.disabled=Your current shader pack has already set a render distance for shadows; you cannot change it. options.iris.gui.hide=Hide GUI options.iris.gui.show=Show GUI +options.iris.search.button=Search +options.iris.clear.button=Clear +options.iris.search.tooltip=Search Shader Settings (Ctrl + F) +options.iris.clear.tooltip=Exit Shader Search (Esc) options.dynamic_lights=Dynamic Lights options.dynamic_lights.tooltip=Enables dynamic lights. Modes: OFF - off, FASTEST - 500ms delay, FAST - 250ms delay, FANCY - 50ms delay, REALTIME - no delay. diff --git a/src/main/resources/assets/angelica/lang/ru_RU.lang b/src/main/resources/assets/angelica/lang/ru_RU.lang index 3e5836c7a..044897114 100644 --- a/src/main/resources/assets/angelica/lang/ru_RU.lang +++ b/src/main/resources/assets/angelica/lang/ru_RU.lang @@ -132,3 +132,7 @@ pack.iris.select.title=Выбрать pack.iris.configure.title=Настроить label.iris.true=Вкл label.iris.false=Выкл +options.iris.search.button=Поиск +options.iris.clear.button=Очистить +options.iris.search.tooltip=Поиск настроек шейдеров (Ctrl + F) +options.iris.clear.tooltip=Выход из поиска шейдеров (Esc) diff --git a/src/main/resources/assets/angelica/lang/zh_CN.lang b/src/main/resources/assets/angelica/lang/zh_CN.lang index 443b780ca..1ff8fe412 100644 --- a/src/main/resources/assets/angelica/lang/zh_CN.lang +++ b/src/main/resources/assets/angelica/lang/zh_CN.lang @@ -138,6 +138,10 @@ options.iris.shadowDistance.enabled=更改渲染阴影的最远距离。超出 options.iris.shadowDistance.disabled=当前光影包已重设阴影渲染距离,无法更改 options.iris.gui.hide=隐藏 GUI options.iris.gui.show=显示 GUI +options.iris.search.button=搜索 +options.iris.clear.button=清除搜索栏 +options.iris.search.tooltip=搜索光影包设定 (Ctrl + F) +options.iris.clear.tooltip=退出搜索 (Esc) options.dynamic_lights=动态光源 options.dynamic_lights.tooltip=启用动态光源,模式选择:OFF-关闭,FASTEST-500ms延迟,FAST-250ms延迟,FANCY-50ms延迟,REALTIME-无延迟