diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java index 3ff6caf20..9bcaf73c3 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/REIConfigScreen.java @@ -31,6 +31,7 @@ import me.shedaniel.clothconfig2.api.ModifierKeyCode; import me.shedaniel.math.Point; import me.shedaniel.math.Rectangle; +import me.shedaniel.math.impl.PointHelper; import me.shedaniel.rei.api.client.REIRuntime; import me.shedaniel.rei.api.client.gui.widgets.Widget; import me.shedaniel.rei.api.client.gui.widgets.Widgets; @@ -43,16 +44,22 @@ import me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl; import me.shedaniel.rei.impl.client.gui.config.components.ConfigCategoriesListWidget; import me.shedaniel.rei.impl.client.gui.config.components.ConfigEntriesListWidget; +import me.shedaniel.rei.impl.client.gui.config.components.ConfigSearchListWidget; import me.shedaniel.rei.impl.client.gui.config.options.AllREIConfigCategories; import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption; import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory; import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup; import me.shedaniel.rei.impl.client.gui.modules.Menu; +import me.shedaniel.rei.impl.client.gui.widget.HoleWidget; +import me.shedaniel.rei.impl.client.gui.widget.basewidgets.TextFieldWidget; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.TextComponent; import net.minecraft.network.chat.TranslatableComponent; +import org.apache.commons.compress.harmony.unpack200.bytecode.forms.WideForm; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; @@ -63,15 +70,17 @@ import java.util.Map; import java.util.function.BiConsumer; +import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.literal; import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.translatable; public class REIConfigScreen extends Screen implements ConfigAccess { private final Screen parent; private final List<OptionCategory> categories; private final List<Widget> widgets = new ArrayList<>(); - private final Map<CompositeOption<?>, ?> defaultOptions = new HashMap<>(); - private final Map<CompositeOption<?>, ?> options = new HashMap<>(); + private final Map<String, ?> defaultOptions = new HashMap<>(); + private final Map<String, ?> options = new HashMap<>(); private OptionCategory activeCategory; + private boolean searching; @Nullable private Menu menu; @Nullable @@ -95,8 +104,8 @@ public REIConfigScreen(Screen parent, List<OptionCategory> categories) { for (OptionCategory category : this.categories) { for (OptionGroup group : category.getGroups()) { for (CompositeOption<?> option : group.getOptions()) { - ((Map<CompositeOption<?>, Object>) this.defaultOptions).put(option, option.getBind().apply(defaultConfig)); - ((Map<CompositeOption<?>, Object>) this.options).put(option, option.getBind().apply(config)); + ((Map<String, Object>) this.defaultOptions).put(option.getId(), option.getBind().apply(defaultConfig)); + ((Map<String, Object>) this.options).put(option.getId(), option.getBind().apply(config)); } } } @@ -111,7 +120,7 @@ private void cleanRequiresLevel() { for (OptionGroup group : category.getGroups()) { group.getOptions().replaceAll(option -> { if (option.isRequiresLevel()) { - return new CompositeOption<>(option.getName(), option.getDescription(), i -> 0, (i, v) -> new Object()) + return new CompositeOption<>(option.getId(), option.getName(), option.getDescription(), i -> 0, (i, v) -> new Object()) .entry(value -> translatable("config.rei.texts.requires_level").withStyle(ChatFormatting.RED)) .defaultValue(() -> 1); } else { @@ -128,27 +137,53 @@ public void init() { this.widgets.clear(); this.widgets.add(Widgets.createLabel(new Point(width / 2, 12), this.title)); int sideWidth = (int) Math.round(width / 4.2); - boolean singlePane = width - 20 - sideWidth <= 330; - int singleSideWidth = 32 + 6 + 4; - Mutable<Widget> list = new MutableObject<>(createEntriesList(singlePane, singleSideWidth, sideWidth)); - IntValue selectedCategory = new IntValue() { - @Override - public void accept(int index) { - REIConfigScreen.this.activeCategory = categories.get(index); - list.setValue(createEntriesList(singlePane, singleSideWidth, sideWidth)); - } - - @Override - public int getAsInt() { - return categories.indexOf(activeCategory); - } - }; - if (!singlePane) { - this.widgets.add(ConfigCategoriesListWidget.create(new Rectangle(8, 32, sideWidth, height - 32 - 32), categories, selectedCategory)); + if (this.searching) { + this.widgets.add(Widgets.createButton(new Rectangle(8, 32, sideWidth, 20), literal("↩ ").append(translatable("gui.back"))) + .onClick(button -> setSearching(false))); + this.widgets.add(HoleWidget.createBackground(new Rectangle(8 + sideWidth + 4, 32, width - 16 - sideWidth - 4, 20), () -> 0, 32)); + TextFieldWidget textField = new TextFieldWidget(new Rectangle(8 + sideWidth + 4 + 6, 32 + 6, width - 16 - sideWidth - 4 - 10, 12)) { + @Override + protected void renderSuggestion(PoseStack matrices, int x, int y) { + int color; + if (containsMouse(PointHelper.ofMouse()) || isFocused()) { + color = 0xddeaeaea; + } else { + color = -6250336; + } + this.font.drawShadow(matrices, this.font.plainSubstrByWidth(this.getSuggestion(), this.getWidth()), x, y, color); + } + }; + textField.setHasBorder(false); + textField.setMaxLength(9000); + this.widgets.add(textField); + this.widgets.add(Widgets.createDrawableWidget((helper, matrices, mouseX, mouseY, delta) -> { + textField.setSuggestion(!textField.isFocused() && textField.getText().isEmpty() ? I18n.get("config.rei.texts.search_options") : null); + })); + this.widgets.add(ConfigSearchListWidget.create(this, this.categories, textField, new Rectangle(8, 32 + 20 + 4, width - 16, height - 32 - (32 + 20 + 4)))); } else { - this.widgets.add(ConfigCategoriesListWidget.createTiny(new Rectangle(8, 32, singleSideWidth - 4, height - 32 - 32), categories, selectedCategory)); + boolean singlePane = width - 20 - sideWidth <= 330; + int singleSideWidth = 32 + 6 + 4; + Mutable<Widget> list = new MutableObject<>(createEntriesList(singlePane, singleSideWidth, sideWidth)); + IntValue selectedCategory = new IntValue() { + @Override + public void accept(int index) { + REIConfigScreen.this.activeCategory = categories.get(index); + list.setValue(createEntriesList(singlePane, singleSideWidth, sideWidth)); + } + + @Override + public int getAsInt() { + return categories.indexOf(activeCategory); + } + }; + if (!singlePane) { + this.widgets.add(ConfigCategoriesListWidget.create(new Rectangle(8, 32, sideWidth, height - 32 - 32), categories, selectedCategory)); + } else { + this.widgets.add(ConfigCategoriesListWidget.createTiny(new Rectangle(8, 32, singleSideWidth - 4, height - 32 - 32), categories, selectedCategory)); + } + this.widgets.add(Widgets.delegate(list::getValue)); } - this.widgets.add(Widgets.delegate(list::getValue)); + this.widgets.add(Widgets.createButton(new Rectangle(width / 2 - 150 - 10, height - 26, 150, 20), translatable("gui.cancel")).onClick(button -> { Minecraft.getInstance().setScreen(this.parent); })); @@ -175,11 +210,11 @@ private Widget createEntriesList(boolean singlePane, int singleSideWidth, int si return ConfigEntriesListWidget.create(this, new Rectangle(singlePane ? 8 + singleSideWidth : 12 + sideWidth, 32, singlePane ? width - 16 - singleSideWidth : width - 20 - sideWidth, height - 32 - 32), activeCategory.getGroups()); } - public Map<CompositeOption<?>, ?> getDefaultOptions() { + public Map<String, ?> getDefaultOptions() { return defaultOptions; } - public Map<CompositeOption<?>, ?> getOptions() { + public Map<String, ?> getOptions() { return options; } @@ -367,17 +402,17 @@ public void closeMenu() { @Override public <T> T get(CompositeOption<T> option) { - return (T) getOptions().get(option); + return (T) getOptions().get(option.getId()); } @Override public <T> void set(CompositeOption<T> option, T value) { - ((Map<CompositeOption<?>, Object>) getOptions()).put(option, value); + ((Map<String, Object>) getOptions()).put(option.getId(), value); } @Override public <T> T getDefault(CompositeOption<T> option) { - return (T) getDefaultOptions().get(option); + return (T) getDefaultOptions().get(option.getId()); } @Override @@ -397,4 +432,13 @@ public void focusKeycode(CompositeOption<ModifierKeyCode> option) { public CompositeOption<ModifierKeyCode> getFocusedKeycode() { return this.focusedKeycodeOption; } + + public void setSearching(boolean searching) { + this.searching = searching; + this.init(this.minecraft, this.width, this.height); + } + + public boolean isSearching() { + return searching; + } } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java index 3ab77951c..072c73a8d 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigCategoriesListWidget.java @@ -27,30 +27,56 @@ import me.shedaniel.math.Rectangle; import me.shedaniel.rei.api.client.gui.widgets.Widget; import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds; +import me.shedaniel.rei.api.common.util.CollectionUtils; import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory; import me.shedaniel.rei.impl.client.gui.widget.ListWidget; import me.shedaniel.rei.impl.client.gui.widget.ScrollableViewWidget; import me.shedaniel.rei.impl.common.util.RectangleUtils; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; import java.util.List; public class ConfigCategoriesListWidget { public static Widget create(Rectangle bounds, List<OptionCategory> categories, IntValue selected) { - WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, 3, 5), categories, - (index, entry) -> ConfigCategoryEntryWidget.create(entry, bounds.width - 6)) + final Mutable<WidgetWithBounds> list = new MutableObject<>(null); + list.setValue(ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, 3, 5), + CollectionUtils.concatUnmodifiable(List.of(ConfigSearchWidget.create(() -> list.getValue() != null && list.getValue().getBounds().height + 6 > bounds.height ? bounds.width - 6 - 6 : bounds.width - 6)), + CollectionUtils.map(categories, entry -> ConfigCategoryEntryWidget.create(entry, bounds.width - 6)))) .gap(3) - .isSelectable((index, entry) -> true) - .selected(selected) - .build(); - return ScrollableViewWidget.create(bounds, list.withPadding(0, 5), true); + .isSelectable((index, entry) -> index != 0) + .selected(new IntValue() { + @Override + public void accept(int value) { + selected.accept(value - 1); + } + + @Override + public int getAsInt() { + return selected.getAsInt() + 1; + } + }) + .build()); + return ScrollableViewWidget.create(bounds, list.getValue().withPadding(0, 5), true); } public static Widget createTiny(Rectangle bounds, List<OptionCategory> categories, IntValue selected) { - WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, (bounds.width - 6 - 16) / 2, 9), categories, - (index, entry) -> ConfigCategoryEntryWidget.createTiny(entry)) + WidgetWithBounds list = ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, (bounds.width - 6 - 16) / 2, 9), + CollectionUtils.concatUnmodifiable(List.of(ConfigSearchWidget.createTiny()), + CollectionUtils.map(categories, ConfigCategoryEntryWidget::createTiny))) .gap(7) - .isSelectable((index, entry) -> true) - .selected(selected) + .isSelectable((index, entry) -> index != 0) + .selected(new IntValue() { + @Override + public void accept(int value) { + selected.accept(value - 1); + } + + @Override + public int getAsInt() { + return selected.getAsInt() + 1; + } + }) .build(); return ScrollableViewWidget.create(bounds, list.withPadding(0, 9), true); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java index b1ecca932..906148b75 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigEntriesListWidget.java @@ -37,7 +37,7 @@ public class ConfigEntriesListWidget { public static Widget create(ConfigAccess access, Rectangle bounds, List<OptionGroup> groups) { WidgetWithBounds list = ListWidget.builderOf(RectangleUtils.inset(bounds, 6, 6), groups, - (index, entry) -> ConfigGroupWidget.create(access, entry, bounds.width - 12 - 6)) + (index, entry) -> ConfigGroupWidget.create(access, entry, bounds.width - 12 - 6, false)) .gap(7) .calculateTotalHeightDynamically(true) .build(); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java index 063596f1a..e41c233b2 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigGroupWidget.java @@ -37,6 +37,7 @@ import me.shedaniel.rei.impl.client.gui.config.options.preview.AccessibilityDisplayPreviewer; import me.shedaniel.rei.impl.client.gui.config.options.preview.InterfacePreviewer; import me.shedaniel.rei.impl.client.gui.config.options.preview.TooltipPreviewer; +import me.shedaniel.rei.impl.client.gui.text.TextTransformations; import net.minecraft.client.gui.GuiComponent; import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.Nullable; @@ -59,13 +60,13 @@ public static void addPreview(OptionGroup group, PreviewLocation location, Speci SPECIAL_GROUPS.put(group, Pair.of(location, constructor)); } - public static WidgetWithBounds create(ConfigAccess access, OptionGroup entry, int width) { - WidgetWithBounds groupTitle = Widgets.createLabel(new Point(0, 3), entry.getGroupName().copy().withStyle(style -> style.withColor(0xFFC0C0C0).withUnderlined(true))) + public static WidgetWithBounds create(ConfigAccess access, OptionGroup entry, int width, boolean applyPreview) { + WidgetWithBounds groupTitle = Widgets.createLabel(new Point(0, 3), TextTransformations.highlightText(entry.getGroupName().copy(), entry.getGroupNameHighlight(), style -> style.withColor(0xFFC0C0C0).withUnderlined(true))) .leftAligned() .withPadding(0, 0, 0, 6); WidgetWithBounds contents; - if (SPECIAL_GROUPS.containsKey(entry)) { + if (applyPreview && SPECIAL_GROUPS.containsKey(entry)) { Pair<PreviewLocation, SpecialGroupConstructor> pair = SPECIAL_GROUPS.get(entry); PreviewLocation location = pair.getLeft(); int halfWidth = width * 6 / 10 - 2; diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java index a449700a0..f2a47a332 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigOptionWidget.java @@ -38,6 +38,7 @@ import me.shedaniel.rei.impl.client.gui.config.ConfigAccess; import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption; import me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils; +import me.shedaniel.rei.impl.client.gui.text.TextTransformations; import net.minecraft.Util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -57,17 +58,17 @@ public static <T> WidgetWithBounds create(ConfigAccess access, CompositeOption<T int[] stableHeight = {12}; int[] height = {12}; Label fieldNameLabel; - widgets.add(fieldNameLabel = Widgets.createLabel(new Point(0, 0), option.getName().copy().withStyle(style -> style.withColor(0xFFC0C0C0))) + widgets.add(fieldNameLabel = Widgets.createLabel(new Point(0, 0), TextTransformations.highlightText(option.getName().copy(), option.getOptionNameHighlight(), style -> style.withColor(0xFFC0C0C0))) .leftAligned()); WidgetWithBounds optionValue = ConfigOptionValueWidget.create(access, option, width - 10 - fieldNameLabel.getBounds().width); widgets.add(Widgets.withTranslate(optionValue, () -> Matrix4f.createTranslateMatrix(width - optionValue.getBounds().width - optionValue.getBounds().x, 0, 0))); widgets.add(new WidgetWithBounds() { final MutableComponent description = Util.make(() -> { - MutableComponent description = option.getDescription().copy().withStyle(style -> style.withColor(0xFF757575)); + MutableComponent description = option.getDescription().copy(); if (description.getString().endsWith(".desc")) { return literal(""); } else { - return description; + return TextTransformations.highlightText(description, option.getOptionDescriptionHighlight(), style -> style.withColor(0xFF757575)); } }); diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java new file mode 100644 index 000000000..034203b88 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchListWidget.java @@ -0,0 +1,127 @@ +package me.shedaniel.rei.impl.client.gui.config.components; + +import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.gui.widgets.TextField; +import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds; +import me.shedaniel.rei.api.client.gui.widgets.Widgets; +import me.shedaniel.rei.impl.client.gui.config.ConfigAccess; +import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption; +import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory; +import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup; +import me.shedaniel.rei.impl.client.gui.widget.ListWidget; +import me.shedaniel.rei.impl.client.gui.widget.ScrollableViewWidget; +import me.shedaniel.rei.impl.client.gui.widget.basewidgets.TextFieldWidget; +import me.shedaniel.rei.impl.common.util.RectangleUtils; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +public class ConfigSearchListWidget { + public static WidgetWithBounds create(ConfigAccess access, List<OptionCategory> categories, TextField textField, Rectangle bounds) { + Mutable<WidgetWithBounds> list = new MutableObject<>(null); + Consumer<String> responder = string -> { + Collection<ConfigSearchWidget.SearchResult> results = ConfigSearchWidget.matchResult(categories, string); + list.setValue(createList(access, results, string, bounds)); + }; + ((TextFieldWidget) textField).setResponder(responder); + responder.accept(textField.getText()); + + return ScrollableViewWidget.create(bounds, Widgets.delegateWithBounds(list::getValue), true); + } + + private static WidgetWithBounds createList(ConfigAccess access, Collection<ConfigSearchWidget.SearchResult> results, String searchTerm, Rectangle bounds) { + return ListWidget.builderOfWidgets(RectangleUtils.inset(bounds, 6, 6), + collectResultWidgets(access, results, searchTerm, bounds)) + .gap(7) + .calculateTotalHeightDynamically(true) + .build() + .withPadding(0, 5); + } + + private static List<WidgetWithBounds> collectResultWidgets(ConfigAccess access, Collection<ConfigSearchWidget.SearchResult> results, String searchTerm, Rectangle bounds) { + List<ConfigSearchWidget.SearchResult> collapsedResults = new ArrayList<>(); + for (ConfigSearchWidget.SearchResult result : results) { + if (result instanceof ConfigSearchWidget.IndividualResult individualResult) { + int lastMatchGroup = -1; + for (int i = 0; i < collapsedResults.size(); i++) { + ConfigSearchWidget.SearchResult prev = collapsedResults.get(i); + if (prev instanceof ConfigSearchWidget.IndividualResult prevInd && prevInd.group().getGroupName().getString().equals(individualResult.group().getGroupName().getString())) { + lastMatchGroup = i; + } + } + if (lastMatchGroup == -1) { + collapsedResults.add(result); + } else { + collapsedResults.add(lastMatchGroup + 1, result); + } + } else { + collapsedResults.add(result); + } + } + + List<WidgetWithBounds> widgets = new ArrayList<>(); + ConfigSearchWidget.SearchResult last = null; + List<CompositeOption<?>> merge = null; + for (ConfigSearchWidget.SearchResult result : collapsedResults) { + if (last instanceof ConfigSearchWidget.IndividualResult lastInd && result instanceof ConfigSearchWidget.IndividualResult currInd) { + if (lastInd.group().getGroupName().getString().equals(currInd.group().getGroupName().getString())) { + if (merge != null) { + merge.add(((ConfigSearchWidget.IndividualResult) currInd.decompose(searchTerm)).option()); + } else { + merge = new ArrayList<>(List.of(((ConfigSearchWidget.IndividualResult) lastInd.decompose(searchTerm)).option(), + ((ConfigSearchWidget.IndividualResult) currInd.decompose(searchTerm)).option())); + } + + last = result; + continue; + } + } + + if (last != null) { + // Commit last + if (merge != null) { + OptionGroup group = ((ConfigSearchWidget.IndividualResult) last).group().copy(); + group.getOptions().clear(); + group.getOptions().addAll(merge); + merge = null; + widgets.add(createSearchResult(access, group, bounds.width - 12 - 6)); + } else { + widgets.add(createSearchResult(access, last.decompose(searchTerm), bounds.width - 12 - 6)); + } + } + last = result; + } + + if (last != null) { + // Commit last + if (merge != null) { + OptionGroup group = ((ConfigSearchWidget.IndividualResult) last).group().copy(); + group.getOptions().clear(); + group.getOptions().addAll(merge); + widgets.add(createSearchResult(access, group, bounds.width - 12 - 6)); + } else { + widgets.add(createSearchResult(access, last.decompose(searchTerm), bounds.width - 12 - 6)); + } + } + + return widgets; + } + + private static WidgetWithBounds createSearchResult(ConfigAccess access, Object result, int width) { + if (result instanceof OptionCategory category) return Widgets.noOp(); + if (result instanceof OptionGroup group) { + return ConfigGroupWidget.create(access, group, width, false); + } + if (result instanceof ConfigSearchWidget.IndividualResult individualResult) { + OptionGroup group = individualResult.group().copy(); + group.getOptions().clear(); + group.getOptions().add(individualResult.option()); + return ConfigGroupWidget.create(access, group, width, false); + } + return Widgets.noOp(); + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java new file mode 100644 index 000000000..e0012a575 --- /dev/null +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/components/ConfigSearchWidget.java @@ -0,0 +1,373 @@ +package me.shedaniel.rei.impl.client.gui.config.components; + +import com.mojang.blaze3d.vertex.PoseStack; +import me.shedaniel.math.Point; +import me.shedaniel.math.Rectangle; +import me.shedaniel.rei.api.client.gui.widgets.Label; +import me.shedaniel.rei.api.client.gui.widgets.Widget; +import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds; +import me.shedaniel.rei.api.client.gui.widgets.Widgets; +import me.shedaniel.rei.impl.client.gui.config.REIConfigScreen; +import me.shedaniel.rei.impl.client.gui.config.options.CompositeOption; +import me.shedaniel.rei.impl.client.gui.config.options.OptionCategory; +import me.shedaniel.rei.impl.client.gui.config.options.OptionGroup; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.resources.ResourceLocation; + +import java.util.*; +import java.util.function.IntSupplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.translatable; + +public class ConfigSearchWidget { + public static WidgetWithBounds create(IntSupplier width) { + Label label = Widgets.createLabel(new Point(21, 6), translatable("config.rei.texts.search_options")) + .leftAligned(); + Font font = Minecraft.getInstance().font; + Rectangle bounds = new Rectangle(0, 0, label.getBounds().getMaxX(), 7 * 3); + return Widgets.concatWithBounds( + bounds, + new Widget() { + @Override + public void render(PoseStack poses, int mouseX, int mouseY, float delta) { + boolean hovering = new Rectangle(-1, -1, width.getAsInt() + 2, 21).contains(mouseX, mouseY); + for (Widget widget : List.of(Widgets.createFilledRectangle(new Rectangle(1, 1, width.getAsInt() - 2, 18), hovering ? 0x50FFFFFF : 0x25FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-1, -1, width.getAsInt() + 2, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-1, 20, width.getAsInt() + 2, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-1, 0, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(width.getAsInt(), 0, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF))) { + widget.render(poses, mouseX, mouseY, delta); + } + label.setColor(hovering ? 0xFFE1E1E1 : 0xFFC0C0C0); + } + + @Override + public List<? extends GuiEventListener> children() { + return List.of(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (new Rectangle(-1, -1, width.getAsInt() + 2, 21).contains(mouseX, mouseY)) { + Widgets.produceClickSound(); + ((REIConfigScreen) Minecraft.getInstance().screen).setSearching(true); + return true; + } + + return false; + } + }, + Widgets.withTranslate(label, 0, 0.5, 0), + Widgets.createTexturedWidget(new ResourceLocation("roughlyenoughitems:textures/gui/config/search_options.png"), new Rectangle(3, 3, 16, 16), 0, 0, 1, 1, 1, 1) + + ); + } + + public static WidgetWithBounds createTiny() { + Rectangle bounds = new Rectangle(0, 0, 16, 16); + return Widgets.withTooltip(Widgets.concatWithBounds( + bounds, + new Widget() { + @Override + public void render(PoseStack poses, int mouseX, int mouseY, float delta) { + boolean hovering = new Rectangle(-1, -1, 18, 18).contains(mouseX, mouseY); + poses.pushPose(); + poses.translate(-0.5, -0.5, 0); + for (Widget widget : List.of(Widgets.createFilledRectangle(new Rectangle(-1, -1, 18, 18), hovering ? 0x50FFFFFF : 0x25FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-3, -3, 22, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-3, 18, 22, 1), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(-3, -2, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF), + Widgets.createFilledRectangle(new Rectangle(18, -2, 1, 20), hovering ? 0x90FFFFFF : 0x45FFFFFF))) { + widget.render(poses, mouseX, mouseY, delta); + } + poses.popPose(); + } + + @Override + public List<? extends GuiEventListener> children() { + return List.of(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (new Rectangle(-1, -1, 18, 18).contains(mouseX, mouseY)) { + Widgets.produceClickSound(); + ((REIConfigScreen) Minecraft.getInstance().screen).setSearching(true); + return true; + } + + return false; + } + }, + Widgets.createTexturedWidget(new ResourceLocation("roughlyenoughitems:textures/gui/config/search_options.png"), bounds, 0, 0, 1, 1, 1, 1) + + ), translatable("config.rei.texts.search_options")); + } + + public static Collection<SearchResult> matchResult(List<OptionCategory> categories, String searchTerm) { + if (searchTerm.isBlank()) return Collections.emptyList(); + List<ScoredResult> scoredResults = collectSearchResults(categories, searchTerm); + // Remove categories results for now, we might add it back later. + scoredResults.removeIf(result -> result.result() instanceof CategoryResult); + + // Distinct them. + Set<SearchResult> distinctResults = new LinkedHashSet<>(); + for (ScoredResult result : scoredResults) { + if (result.score() >= 0.001F) { + distinctResults.add(result.result()); + } + } + + // Remove duplicates. + Set<OptionCategory> visitedCategories = new HashSet<>(); + Set<OptionGroup> visitedGroups = new HashSet<>(); + Iterator<SearchResult> iterator = distinctResults.iterator(); + while (iterator.hasNext()) { + SearchResult result = iterator.next(); + if (result instanceof CategoryResult categoryResult) { + visitedCategories.add(categoryResult.category()); + } else if (result instanceof GroupResult groupResult) { + if (visitedCategories.contains(((GroupResult) result).category)) { + iterator.remove(); + } else { + visitedGroups.add(groupResult.group()); + } + } else if (result instanceof IndividualResult individualResult) { + if (visitedCategories.contains(individualResult.category()) || visitedGroups.contains(individualResult.group())) { + iterator.remove(); + } + } + } + + return distinctResults; + } + + private static List<ScoredResult> collectSearchResults(List<OptionCategory> categories, String searchTerm) { + String lcSearchTerm = searchTerm.toLowerCase(Locale.ROOT); + + // Find all options that match. + List<SearchResult> results = new ArrayList<>(); + List<SearchResult> fuzzyResults = new ArrayList<>(); + for (OptionCategory category : categories) { + if (category.getKey().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new CategoryResult(category, new MatchComposite(category.getKey(), MatchType.KEY))); + } else { + fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getKey(), MatchType.KEY))); + } + if (category.getName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new CategoryResult(category, new MatchComposite(category.getName().getString(), MatchType.NAME))); + } else { + fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getName().getString(), MatchType.NAME))); + } + if (!category.getDescription().getString().endsWith(".desc") && category.getDescription().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new CategoryResult(category, new MatchComposite(category.getDescription().getString(), MatchType.DESCRIPTION))); + } else { + fuzzyResults.add(new CategoryResult(category, new MatchComposite(category.getDescription().getString(), MatchType.DESCRIPTION))); + } + + for (OptionGroup group : category.getGroups()) { + if (group.getId().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new GroupResult(category, group, new MatchComposite(group.getId(), MatchType.KEY))); + } else { + fuzzyResults.add(new GroupResult(category, group, new MatchComposite(group.getId(), MatchType.KEY))); + } + if (group.getGroupName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new GroupResult(category, group, new MatchComposite(group.getGroupName().getString(), MatchType.NAME))); + } else { + fuzzyResults.add(new GroupResult(category, group, new MatchComposite(group.getGroupName().getString(), MatchType.NAME))); + } + + for (CompositeOption<?> option : group.getOptions()) { + if (option.getId().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new IndividualResult(category, group, option, new MatchComposite(option.getId(), MatchType.KEY))); + } else { + fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getId(), MatchType.KEY))); + } + if (option.getName().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new IndividualResult(category, group, option, new MatchComposite(option.getName().getString(), MatchType.NAME))); + } else { + fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getName().getString(), MatchType.NAME))); + } + if (!option.getDescription().getString().endsWith(".desc") && option.getDescription().getString().toLowerCase(Locale.ROOT).contains(lcSearchTerm)) { + results.add(new IndividualResult(category, group, option, new MatchComposite(option.getDescription().getString(), MatchType.DESCRIPTION))); + } else { + fuzzyResults.add(new IndividualResult(category, group, option, new MatchComposite(option.getDescription().getString(), MatchType.DESCRIPTION))); + } + } + } + } + + return Stream.concat(results.stream().map(result -> { + return new ScoredResult(result, similarity(result.matched().matched(), searchTerm) * result.matched().type().multiplier()); + }), fuzzyResults.stream().map(result -> { + return new ScoredResult(result, (float) Math.pow(similarity(result.matched().matched(), searchTerm), 1.5) * result.matched().type().multiplier() * 0.9F); + }).filter(result -> result.score() > 0.5F)).sorted(Comparator.comparingDouble(ScoredResult::score).reversed()) + .collect(Collectors.toList()); + } + + public interface SearchResult { + MatchComposite matched(); + + Object decompose(String searchTerm); + } + + public record ScoredResult( + SearchResult result, + float score + ) { + } + + public record IndividualResult( + OptionCategory category, + OptionGroup group, + CompositeOption<?> option, + MatchComposite matched + ) implements SearchResult { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IndividualResult that = (IndividualResult) o; + return Objects.equals(option, that.option); + } + + @Override + public int hashCode() { + return Objects.hash(option); + } + + @Override + public Object decompose(String searchTerm) { + CompositeOption<?> copied = option().copy(); + if (matched().type() == MatchType.NAME) copied.setOptionNameHighlight(searchTerm); + else if (matched().type() == MatchType.DESCRIPTION) copied.setOptionDescriptionHighlight(searchTerm); + return new IndividualResult(category(), group(), copied, matched()); + } + } + + public record GroupResult( + OptionCategory category, + OptionGroup group, + MatchComposite matched + ) implements SearchResult { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GroupResult that = (GroupResult) o; + return Objects.equals(group, that.group); + } + + @Override + public int hashCode() { + return Objects.hash(group); + } + + @Override + public Object decompose(String searchTerm) { + OptionGroup copied = this.group().copy(); + copied.setGroupNameHighlight(searchTerm); + return copied; + } + } + + public record CategoryResult( + OptionCategory category, + MatchComposite matched + ) implements SearchResult { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CategoryResult that = (CategoryResult) o; + return Objects.equals(category, that.category); + } + + @Override + public int hashCode() { + return Objects.hash(category); + } + + @Override + public Object decompose(String searchTerm) { + return this.category(); + } + } + + public enum MatchType { + KEY(1.0F), + NAME(0.98F), + DESCRIPTION(0.8F), + ; + + private final float multiplier; + + MatchType(float multiplier) { + this.multiplier = multiplier; + } + + public float multiplier() { + return multiplier; + } + } + + public record MatchComposite( + String matched, + MatchType type + ) { + } + + private static float similarity(String first, String second) { + String firstLowerCase = first.toLowerCase(Locale.ROOT); + String secondLowerCase = second.toLowerCase(Locale.ROOT); + if (!Objects.equals(first, firstLowerCase) || !Objects.equals(second, secondLowerCase)) { + return (innerSimilarity(first, second) + innerSimilarity(firstLowerCase, secondLowerCase)) / 2.0F; + } else { + return innerSimilarity(first, second); + } + } + + private static float innerSimilarity(String first, String second) { + String longer = first; + String shorter = second; + if (first.length() < second.length()) { + longer = second; + shorter = first; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1.0F; + } else { + return (longerLength - editDistance(longer, shorter)) / ((float) longerLength); + } + } + + private static int editDistance(String s11, String s22) { + int[] costs = new int[s22.length() + 1]; + for (int i = 0; i <= s11.length(); i++) { + int lastValue = i; + for (int j = 0; j <= s22.length(); j++) { + if (i == 0) { + costs[j] = j; + } else { + if (j > 0) { + int newValue = costs[j - 1]; + if (s11.charAt(i - 1) != s22.charAt(j - 1)) { + newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; + } + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) { + costs[s22.length()] = lastValue; + } + } + return costs[s22.length()]; + } +} diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java index 469013a0a..9122bf1d4 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigCategories.java @@ -33,7 +33,7 @@ public interface AllREIConfigCategories { static OptionCategory make(String key) { - return OptionCategory.of(new ResourceLocation("roughlyenoughitems:textures/gui/config/" + key + ".png"), + return OptionCategory.of(key, new ResourceLocation("roughlyenoughitems:textures/gui/config/" + key + ".png"), translatable("config.rei.categories." + key), translatable("config.rei.categories." + key + ".desc")); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java index 32e2a002d..9e392d8df 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigGroups.java @@ -28,7 +28,7 @@ public interface AllREIConfigGroups { static <T> OptionGroup make(String id) { - return new OptionGroup(translatable("config.rei.options.groups." + id)); + return new OptionGroup(id, translatable("config.rei.options.groups." + id)); } OptionGroup APPEARANCE_INTERFACE = make("appearance.interface") diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java index b534d40e4..f09cefdd4 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/AllREIConfigOptions.java @@ -57,7 +57,7 @@ public interface AllREIConfigOptions { static <T> CompositeOption<T> make(String id, Function<ConfigObjectImpl, T> bind, BiConsumer<ConfigObjectImpl, T> save) { - return new CompositeOption<>(translatable("config.rei.options." + id), + return new CompositeOption<>(id, translatable("config.rei.options." + id), translatable("config.rei.options." + id + ".desc"), bind, save); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java index fed950b48..e9f932759 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/CompositeOption.java @@ -33,6 +33,7 @@ import java.util.function.Supplier; public class CompositeOption<T> { + private final String id; private final Component name; private final Component description; private final Function<ConfigObjectImpl, T> bind; @@ -43,8 +44,13 @@ public class CompositeOption<T> { private Supplier<T> defaultValue = null; private OptionValueEntry<T> entry = OptionValueEntry.noOp(); private boolean requiresLevel = false; + @Nullable + private String optionNameHighlight = null; + @Nullable + private String optionDescriptionHighlight = null; - public CompositeOption(Component name, Component description, Function<ConfigObjectImpl, T> bind, BiConsumer<ConfigObjectImpl, T> save) { + public CompositeOption(String id, Component name, Component description, Function<ConfigObjectImpl, T> bind, BiConsumer<ConfigObjectImpl, T> save) { + this.id = id; this.name = name; this.description = description; this.bind = bind; @@ -101,10 +107,28 @@ public CompositeOption<T> requiresLevel() { return this; } + public void setOptionNameHighlight(@Nullable String optionNameHighlight) { + this.optionNameHighlight = optionNameHighlight; + } + + public void setOptionDescriptionHighlight(@Nullable String optionDescriptionHighlight) { + this.optionDescriptionHighlight = optionDescriptionHighlight; + } + public boolean isRequiresLevel() { return requiresLevel; } + @Nullable + public String getOptionNameHighlight() { + return optionNameHighlight; + } + + @Nullable + public String getOptionDescriptionHighlight() { + return optionDescriptionHighlight; + } + public CompositeOption<T> previewer(ConfigPreviewer<T> previewer) { this.previewer = previewer; return this; @@ -115,6 +139,10 @@ public CompositeOption<T> defaultValue(Supplier<T> defaultValue) { return this; } + public String getId() { + return id; + } + public Component getName() { return name; } @@ -151,7 +179,7 @@ public T getDefaultValue() { } public CompositeOption<T> copy() { - CompositeOption<T> option = new CompositeOption<>(name, description, bind, save); + CompositeOption<T> option = new CompositeOption<>(id, name, description, bind, save); option.entry = entry; option.previewer = previewer; option.defaultValue = defaultValue; diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java index 5a9eaaf6a..536b39892 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionCategory.java @@ -30,19 +30,21 @@ import java.util.List; public class OptionCategory { + private final String key; private final ResourceLocation icon; private final Component name; private final Component description; private final List<OptionGroup> groups = new ArrayList<>(); - private OptionCategory(ResourceLocation icon, Component name, Component description) { + private OptionCategory(String key, ResourceLocation icon, Component name, Component description) { + this.key = key; this.icon = icon; this.name = name; this.description = description; } - public static OptionCategory of(ResourceLocation icon, Component name, Component description) { - return new OptionCategory(icon, name, description); + public static OptionCategory of(String key, ResourceLocation icon, Component name, Component description) { + return new OptionCategory(key, icon, name, description); } public OptionCategory add(OptionGroup group) { @@ -50,6 +52,10 @@ public OptionCategory add(OptionGroup group) { return this; } + public String getKey() { + return key; + } + public ResourceLocation getIcon() { return icon; } @@ -67,7 +73,7 @@ public List<OptionGroup> getGroups() { } public OptionCategory copy() { - OptionCategory category = new OptionCategory(icon, name, description); + OptionCategory category = new OptionCategory(key, icon, name, description); for (OptionGroup group : groups) { category.add(group.copy()); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java index 4123712b4..8830bef33 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/config/options/OptionGroup.java @@ -24,16 +24,21 @@ package me.shedaniel.rei.impl.client.gui.config.options; import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class OptionGroup { + private final String id; private final Component groupName; private final List<CompositeOption<?>> options = new ArrayList<>(); + @Nullable + private String groupNameHighlight = null; - public OptionGroup(Component groupName) { + public OptionGroup(String id, Component groupName) { + this.id = id; this.groupName = groupName; } @@ -42,6 +47,14 @@ public OptionGroup add(CompositeOption<?> option) { return this; } + public void setGroupNameHighlight(@Nullable String groupNameHighlight) { + this.groupNameHighlight = groupNameHighlight; + } + + public String getId() { + return id; + } + public Component getGroupName() { return groupName; } @@ -50,6 +63,11 @@ public List<CompositeOption<?>> getOptions() { return options; } + @Nullable + public String getGroupNameHighlight() { + return groupNameHighlight; + } + @Override public boolean equals(Object obj) { if (obj instanceof OptionGroup group) { @@ -65,7 +83,7 @@ public int hashCode() { } public OptionGroup copy() { - OptionGroup group = new OptionGroup(groupName); + OptionGroup group = new OptionGroup(id, groupName); for (CompositeOption<?> option : options) { group.add(option); } diff --git a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java index 1a15d4583..d7a8d0a1c 100644 --- a/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java +++ b/runtime/src/main/java/me/shedaniel/rei/impl/client/gui/text/TextTransformations.java @@ -24,12 +24,20 @@ package me.shedaniel.rei.impl.client.gui.text; import me.shedaniel.math.Color; +import net.minecraft.ChatFormatting; import net.minecraft.Util; import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.MutableComponent; import net.minecraft.network.chat.Style; import net.minecraft.network.chat.TextColor; import net.minecraft.util.FormattedCharSequence; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; +import java.util.function.UnaryOperator; + +import static me.shedaniel.rei.impl.client.gui.config.options.ConfigUtils.literal; @ApiStatus.Internal public class TextTransformations { @@ -92,6 +100,35 @@ public static FormattedCharSequence forwardWithTransformation(String text, CharS }; } + public static MutableComponent highlightText(MutableComponent component, @Nullable String highlight, UnaryOperator<Style> styleOperator) { + if (highlight == null) return component.withStyle(styleOperator); + String string = component.getString(); + if (string.toLowerCase(Locale.ROOT).equals(highlight.toLowerCase(Locale.ROOT))) { + return component.withStyle(styleOperator).withStyle(ChatFormatting.YELLOW); + } + String[] parts = string.toLowerCase(Locale.ROOT).split(highlight.toLowerCase(Locale.ROOT)); + if (string.toLowerCase(Locale.ROOT).endsWith(highlight.toLowerCase(Locale.ROOT))) { + // Append an empty string to the end + String[] newParts = new String[parts.length + 1]; + System.arraycopy(parts, 0, newParts, 0, parts.length); + newParts[parts.length] = ""; + parts = newParts; + } + if (parts.length <= 1) return component.withStyle(styleOperator); + MutableComponent output = literal(""); + int curr = 0; + for (int i = 0; i < parts.length; i++) { + output.append(literal(string.substring(curr, curr + parts[i].length())).withStyle(styleOperator)); + curr += parts[i].length(); + if (i != parts.length - 1) { + output.append(literal(string.substring(curr, curr + highlight.length())).withStyle(styleOperator) + .withStyle(ChatFormatting.YELLOW)); + curr += highlight.length(); + } + } + return output; + } + @FunctionalInterface public interface CharSequenceTransformer { Style apply(String text, int charIndex, char c); diff --git a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json index 4937b183d..abb922c40 100755 --- a/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json +++ b/runtime/src/main/resources/assets/roughlyenoughitems/lang/en_us.json @@ -462,6 +462,7 @@ "config.rei.value.trueFalse.true": "True", "config.rei.value.enabledDisabled.false": "Disabled", "config.rei.value.enabledDisabled.true": "Enabled", + "config.rei.texts.search_options": "Search options...", "config.rei.texts.preview": "Preview...", "config.rei.texts.configure": "Configure...", "config.rei.texts.details": "Details...", diff --git a/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png b/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png new file mode 100644 index 000000000..b7b3696ee Binary files /dev/null and b/runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/config/search_options.png differ