Przeglądaj źródła

New search syntax highlighting, small changes to dark theme, bump to 5.9.0

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 4 lat temu
rodzic
commit
e5e80c88ae
32 zmienionych plików z 698 dodań i 149 usunięć
  1. 3 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/ConfigObject.java
  2. 43 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/gui/config/SyntaxHighlightingMode.java
  3. 15 9
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/gui/widget/TextFieldWidget.java
  4. 0 28
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  5. 59 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java
  6. 18 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  7. 19 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  8. 3 2
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java
  9. 114 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/config/entry/SearchFilterSyntaxHighlightingEntry.java
  10. 2 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/Menu.java
  11. 1 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  12. 1 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java
  13. 5 4
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java
  14. 10 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  15. 72 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/OverlaySearchFieldSyntaxHighlighter.java
  16. 80 24
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/SearchArgument.java
  17. 55 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/TextTransformations.java
  18. 9 2
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/AlwaysMatchingArgument.java
  19. 15 17
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/Argument.java
  20. 3 3
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/ArgumentsRegistry.java
  21. 29 3
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/MatchStatus.java
  22. 42 11
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/ModArgument.java
  23. 22 10
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/RegexArgument.java
  24. 37 19
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TagArgument.java
  25. 12 5
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TextArgument.java
  26. 21 5
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TooltipArgument.java
  27. 1 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java
  28. BIN
      RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/display.png
  29. BIN
      RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/display_dark.png
  30. BIN
      RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer_dark.png
  31. 1 1
      gradle.properties
  32. 6 0
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json

+ 3 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/ConfigObject.java

@@ -190,4 +190,7 @@ public interface ConfigObject {
     
     @ApiStatus.Experimental
     double getVerticalEntriesBoundaries();
+    
+    @ApiStatus.Experimental
+    SyntaxHighlightingMode getSyntaxHighlightingMode();
 }

+ 43 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/gui/config/SyntaxHighlightingMode.java

@@ -0,0 +1,43 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.resources.language.I18n;
+
+import java.util.Locale;
+
+@Environment(EnvType.CLIENT)
+public enum SyntaxHighlightingMode {
+    PLAIN,
+    PLAIN_UNDERSCORED,
+    COLORFUL,
+    COLORFUL_UNDERSCORED;
+    
+    @Override
+    public String toString() {
+        return I18n.get("config.roughlyenoughitems.syntaxHighlightingMode." + name().toLowerCase(Locale.ROOT));
+    }
+}

+ 15 - 9
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/gui/widget/TextFieldWidget.java

@@ -35,6 +35,8 @@ import me.shedaniel.rei.api.REIHelper;
 import net.minecraft.SharedConstants;
 import net.minecraft.Util;
 import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Style;
+import net.minecraft.util.FormattedCharSequence;
 import net.minecraft.util.Mth;
 import net.minecraft.world.level.block.entity.TickableBlockEntity;
 import org.jetbrains.annotations.ApiStatus;
@@ -42,7 +44,6 @@ import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.function.BiFunction;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -61,7 +62,7 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
     protected int selectionEnd;
     protected int editableColor;
     protected int notEditableColor;
-    protected BiFunction<String, Integer, String> renderTextProvider;
+    protected TextFormatter formatter;
     private Rectangle bounds;
     private String text;
     private int maxLength;
@@ -84,9 +85,7 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
         this.notEditableColor = 7368816;
         this.visible = true;
         this.textPredicate = s -> true;
-        this.renderTextProvider = (string_1, integer_1) -> {
-            return string_1;
-        };
+        this.formatter = TextFormatter.DEFAULT;
         this.bounds = rectangle;
         this.stripInvalid = SharedConstants::filterText;
     }
@@ -113,8 +112,8 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
         this.changedListener = biConsumer_1;
     }
     
-    public void setRenderTextProvider(BiFunction<String, Integer, String> biFunction_1) {
-        this.renderTextProvider = biFunction_1;
+    public void setFormatter(TextFormatter formatter) {
+        this.formatter = formatter;
     }
     
     @Override
@@ -444,7 +443,7 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
             
             if (!string_1.isEmpty()) {
                 String string_2 = boolean_1 ? string_1.substring(0, int_4) : string_1;
-                int_8 = this.font.drawShadow(matrices, this.renderTextProvider.apply(string_2, this.firstCharacterIndex), (float) x, (float) y, color);
+                int_8 = this.font.drawShadow(matrices, this.formatter.format(this, string_2, this.firstCharacterIndex), (float) x, (float) y, color);
             }
             
             boolean isCursorInsideText = this.selectionStart < this.text.length() || this.text.length() >= this.getMaxLength();
@@ -457,7 +456,7 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
             int_9--;
             
             if (!string_1.isEmpty() && boolean_1 && int_4 < string_1.length()) {
-                this.font.drawShadow(matrices, this.renderTextProvider.apply(string_1.substring(int_4), this.selectionStart), (float) int_8, (float) y, color);
+                this.font.drawShadow(matrices, this.formatter.format(this, string_1.substring(int_4), this.selectionStart), (float) int_8, (float) y, color);
             }
             
             if (!isCursorInsideText && text.isEmpty() && this.suggestion != null) {
@@ -631,4 +630,11 @@ public class TextFieldWidget extends WidgetWithBounds implements TickableBlockEn
         return index > this.text.length() ? this.bounds.x : this.bounds.x + this.font.width(this.text.substring(0, index));
     }
     
+    public interface TextFormatter {
+        TextFormatter DEFAULT = (widget, text, index) -> {
+            return FormattedCharSequence.forward(text, Style.EMPTY);
+        };
+        
+        FormattedCharSequence format(TextFieldWidget widget, String text, int index);
+    }
 }

+ 0 - 28
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -89,34 +89,6 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
     private int tooltipWidth;
     private int tooltipHeight;
     private List<FormattedCharSequence> tooltipLines;
-    public final TriConsumer<PoseStack, Point, Float> renderTooltipCallback = (matrices, mouse, aFloat) -> {
-        RenderSystem.disableRescaleNormal();
-        RenderSystem.disableDepthTest();
-        matrices.pushPose();
-        matrices.translate(0, 0, 999);
-        int x = mouse.x;
-        int y = mouse.y;
-        this.fillGradient(matrices, x - 3, y - 4, x + tooltipWidth + 3, y - 3, -267386864, -267386864);
-        this.fillGradient(matrices, x - 3, y + tooltipHeight + 3, x + tooltipWidth + 3, y + tooltipHeight + 4, -267386864, -267386864);
-        this.fillGradient(matrices, x - 3, y - 3, x + tooltipWidth + 3, y + tooltipHeight + 3, -267386864, -267386864);
-        this.fillGradient(matrices, x - 4, y - 3, x - 3, y + tooltipHeight + 3, -267386864, -267386864);
-        this.fillGradient(matrices, x + tooltipWidth + 3, y - 3, x + tooltipWidth + 4, y + tooltipHeight + 3, -267386864, -267386864);
-        this.fillGradient(matrices, x - 3, y - 3 + 1, x - 3 + 1, y + tooltipHeight + 3 - 1, 1347420415, 1344798847);
-        this.fillGradient(matrices, x + tooltipWidth + 2, y - 3 + 1, x + tooltipWidth + 3, y + tooltipHeight + 3 - 1, 1347420415, 1344798847);
-        this.fillGradient(matrices, x - 3, y - 3, x + tooltipWidth + 3, y - 3 + 1, 1347420415, 1347420415);
-        this.fillGradient(matrices, x - 3, y + tooltipHeight + 2, x + tooltipWidth + 3, y + tooltipHeight + 3, 1344798847, 1344798847);
-        int currentY = y;
-        MultiBufferSource.BufferSource immediate = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
-        Matrix4f matrix = matrices.last().pose();
-        for (int lineIndex = 0; lineIndex < tooltipLines.size(); lineIndex++) {
-            font.drawInBatch(tooltipLines.get(lineIndex), x, currentY, -1, true, matrix, immediate, false, 0, 15728880);
-            currentY += lineIndex == 0 ? 12 : 10;
-        }
-        immediate.endBatch();
-        matrices.popPose();
-        RenderSystem.enableDepthTest();
-        RenderSystem.enableRescaleNormal();
-    };
     private Rectangle bounds;
     private Window window;
     private Button leftButton, rightButton;

+ 59 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java

@@ -27,25 +27,41 @@ import com.google.common.collect.Lists;
 import com.mojang.blaze3d.platform.InputConstants;
 import com.mojang.blaze3d.systems.RenderSystem;
 import com.mojang.blaze3d.vertex.PoseStack;
+import me.shedaniel.math.Color;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.gui.config.SyntaxHighlightingMode;
 import me.shedaniel.rei.gui.widget.TextFieldWidget;
+import me.shedaniel.rei.impl.OverlaySearchFieldSyntaxHighlighter;
 import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.impl.TextTransformations;
+import me.shedaniel.rei.impl.search.Argument;
+import me.shedaniel.rei.impl.search.ArgumentsRegistry;
+import me.shedaniel.rei.impl.search.TextArgument;
+import net.minecraft.ChatFormatting;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.resources.language.I18n;
 import net.minecraft.client.resources.sounds.SimpleSoundInstance;
+import net.minecraft.network.chat.Style;
+import net.minecraft.network.chat.TextColor;
 import net.minecraft.sounds.SoundEvents;
+import net.minecraft.util.FormattedCharSequence;
 import net.minecraft.util.Tuple;
 import org.jetbrains.annotations.ApiStatus;
 
 import java.util.List;
+import java.util.function.Consumer;
 
 @ApiStatus.Internal
-public class OverlaySearchField extends TextFieldWidget {
+public class OverlaySearchField extends TextFieldWidget implements TextFieldWidget.TextFormatter {
     
     public static boolean isHighlighting = false;
+    private static final Style SPLITTER_STYLE = Style.EMPTY.withColor(ChatFormatting.GRAY);
+    private static final Style QUOTES_STYLE = Style.EMPTY.withColor(ChatFormatting.GOLD);
+    private static final Style ERROR_STYLE = Style.EMPTY.withColor(TextColor.fromRgb(0xff5555));
+    private final OverlaySearchFieldSyntaxHighlighter highlighter = new OverlaySearchFieldSyntaxHighlighter(this);
     public long keybindFocusTime = -1;
     public int keybindFocusKey = -1;
     public boolean isMain = true;
@@ -55,6 +71,48 @@ public class OverlaySearchField extends TextFieldWidget {
     public OverlaySearchField(int x, int y, int width, int height) {
         super(x, y, width, height);
         setMaxLength(10000);
+        setFormatter(this);
+        super.setChangedListener(highlighter);
+    }
+    
+    @Override
+    public FormattedCharSequence format(TextFieldWidget widget, String text, int index) {
+        boolean isPlain = ConfigObject.getInstance().getSyntaxHighlightingMode() == SyntaxHighlightingMode.PLAIN || ConfigObject.getInstance().getSyntaxHighlightingMode() == SyntaxHighlightingMode.PLAIN_UNDERSCORED;
+        boolean hasUnderscore = ConfigObject.getInstance().getSyntaxHighlightingMode() == SyntaxHighlightingMode.PLAIN_UNDERSCORED || ConfigObject.getInstance().getSyntaxHighlightingMode() == SyntaxHighlightingMode.COLORFUL_UNDERSCORED;
+        return TextTransformations.forwardWithTransformation(text, (s, charIndex, c) -> {
+            byte arg = highlighter.highlighted[charIndex + index];
+            Style style = Style.EMPTY;
+            if (arg > 0) {
+                Argument<?, ?> argument = ArgumentsRegistry.ARGUMENT_LIST.get((arg - 1) / 2);
+                if (isMain && ContainerScreenOverlay.getEntryListWidget().getAllStacks().isEmpty() && !getText().isEmpty()) {
+                    style = ERROR_STYLE;
+                }
+                if (!isPlain) {
+                    style = argument.getHighlightedStyle();
+                }
+                if (!(argument instanceof TextArgument) && hasUnderscore && arg % 2 == 1) {
+                    style = style.withUnderlined(true);
+                }
+            } else if (!isPlain) {
+                if (arg == -1) {
+                    style = SPLITTER_STYLE;
+                } else if (arg == -2) {
+                    style = QUOTES_STYLE;
+                }
+            }
+            
+            
+            if (containsMouse(PointHelper.ofMouse()) || isFocused()) {
+                return style;
+            }
+            int color = this.editable ? this.editableColor : this.notEditableColor;
+            return style.withColor(TextColor.fromRgb(Color.ofOpaque(style.getColor() == null ? color : style.getColor().getValue()).brighter(0.75).getColor()));
+        });
+    }
+    
+    @Override
+    public void setChangedListener(Consumer<String> responder) {
+        super.setChangedListener(highlighter.andThen(responder));
     }
     
     @Override

+ 18 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -338,6 +338,7 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             int innerWidth = Mth.ceil(workingStations.size() / ((float) hh));
             int xx = bounds.x - (8 + innerWidth * 16) + 6;
             int yy = bounds.y + 16;
+//            int yy = bounds.y + 36;
             preWidgets.add(workingStationsBaseWidget = Widgets.createCategoryBase(new Rectangle(xx - 5, yy - 5, 15 + innerWidth * 16, 10 + actualHeight * 16)));
             preWidgets.add(Widgets.createSlotBase(new Rectangle(xx - 1, yy - 1, innerWidth * 16 + 2, actualHeight * 16 + 2)));
             int index = 0;
@@ -353,6 +354,23 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
                 }
             }
         }
+    
+        if (false) {
+            int innerWidth = 10;
+            innerWidth = 19;
+            int actualHeight = 10;
+//            Rectangle base = new Rectangle(bounds.x - (8 + innerWidth) + 6 - 5, bounds.getMaxY() - 26, 15 + innerWidth, 10 + actualHeight);
+            Rectangle base = new Rectangle(bounds.x - (8 + innerWidth) + 6 - 5, bounds.y + 8, 15 + innerWidth, 10 + actualHeight);
+            preWidgets.add(Widgets.createCategoryBase(base));
+            int border = 5;
+            preWidgets.add(Widgets.createButton(new Rectangle(base.x + border, base.y + border, 10, 10), new ImmutableLiteralText("<"))
+                    .tooltipLines("Go back in history", " ", "§7You may alternatively press backspace.")
+                    .onClick(button -> minecraft.setScreen(ScreenHelper.getLastRecipeScreen()))
+                    .enabled(ScreenHelper.hasLastRecipeScreen()));
+            preWidgets.add(Widgets.createButton(new Rectangle(base.x + border + 9, base.y + border, 10, 10), new ImmutableLiteralText(">"))
+                    .tooltipLines("Go forward in history", " ", "§7You may alternatively press backspace.")
+                    .onClick(button -> minecraft.setScreen(ScreenHelper.getLastRecipeScreen())));
+        }
         
         children.addAll(tabs);
         children.addAll(widgets);

+ 19 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -57,6 +57,7 @@ import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.List;
+import java.util.ListIterator;
 import java.util.Map;
 import java.util.Optional;
 
@@ -290,7 +291,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
         double height = scrolling.getMaxScrollHeight();
         if (scrollListBounds.contains(mouseX, mouseY) && height > scrollListBounds.height - 2) {
-            scrolling.offset(ClothConfigInitializer.getScrollStep() * -amount, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -amount * (Screen.hasAltDown() ? 3 : 1), true);
             if (scrollBarAlphaFuture == 0)
                 scrollBarAlphaFuture = 1f;
             if (System.currentTimeMillis() - scrollBarAlphaFutureTime > 300f)
@@ -320,18 +321,35 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
                 if (selectedRecipeIndex >= categoryMap.get(categories.get(selectedCategoryIndex)).size())
                     selectedRecipeIndex = 0;
                 init();
+                scrollToSelected();
                 return true;
             } else if (categoryMap.get(categories.get(selectedCategoryIndex)).size() > 1) {
                 selectedRecipeIndex--;
                 if (selectedRecipeIndex < 0)
                     selectedRecipeIndex = categoryMap.get(categories.get(selectedCategoryIndex)).size() - 1;
                 init();
+                scrollToSelected();
                 return true;
             }
         }
         return super.mouseScrolled(mouseX, mouseY, amount);
     }
     
+    private void scrollToSelected() {
+        int yy = 0;
+        ListIterator<Button> iterator = buttonList.listIterator();
+        while (iterator.hasNext()) {
+            Button button = iterator.next();
+        
+            if (iterator.nextIndex() - 1 == selectedRecipeIndex) {
+                scrolling.scrollTo(Mth.clamp(yy - scrolling.getBounds().height / 2 + button.getBounds().height / 2, 0, scrolling.getMaxScroll()), true);
+                break;
+            }
+        
+            yy += button.getBounds().height;
+        }
+    }
+    
     @Override
     public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
         if (ConfigObject.getInstance().doesVillagerScreenHavePermanentScrollBar()) {

+ 3 - 2
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java

@@ -176,11 +176,12 @@ public class FilteringScreen extends Screen {
         super.init();
         Rectangle bounds = getBounds();
         updateSearch(this.searchField.getText());
-        this.searchField.getBounds().setBounds(bounds.getCenterX() - 75, bounds.getMaxY() - 22, 150, 18);
         this.selectAllButton.x = 2;
         this.selectAllButton.y = bounds.getMaxY() - 22;
         this.selectNoneButton.x = 4 + selectAllButton.getWidth();
         this.selectNoneButton.y = bounds.getMaxY() - 22;
+        int searchFieldWidth = Math.max(bounds.width - (selectNoneButton.x + selectNoneButton.getWidth() + hideButton.getWidth() + showButton.getWidth() + 12), 100);
+        this.searchField.getBounds().setBounds(selectNoneButton.x + selectNoneButton.getWidth() + 4, bounds.getMaxY() - 21, searchFieldWidth, 18);
         this.hideButton.x = bounds.getMaxX() - hideButton.getWidth() - showButton.getWidth() - 4;
         this.hideButton.y = bounds.getMaxY() - 22;
         this.showButton.x = bounds.getMaxX() - showButton.getWidth() - 2;
@@ -442,7 +443,7 @@ public class FilteringScreen extends Screen {
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (getBounds().contains(double_1, double_2)) {
-            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3 * (Screen.hasAltDown() ? 3 : 1), true);
             return true;
         }
         super.mouseScrolled(double_1, double_2, double_3);

+ 114 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/config/entry/SearchFilterSyntaxHighlightingEntry.java

@@ -0,0 +1,114 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import com.google.common.collect.ImmutableList;
+import com.mojang.blaze3d.platform.Window;
+import com.mojang.blaze3d.vertex.PoseStack;
+import me.shedaniel.clothconfig2.gui.entries.TooltipListEntry;
+import me.shedaniel.rei.gui.config.SyntaxHighlightingMode;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.chat.NarratorChatListener;
+import net.minecraft.client.gui.components.AbstractButton;
+import net.minecraft.client.gui.components.AbstractWidget;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TextComponent;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+public class SearchFilterSyntaxHighlightingEntry extends TooltipListEntry<SyntaxHighlightingMode> {
+    private final SyntaxHighlightingMode original;
+    private SyntaxHighlightingMode type;
+    private SyntaxHighlightingMode defaultValue;
+    private Consumer<SyntaxHighlightingMode> save;
+    private final AbstractWidget buttonWidget = new AbstractButton(0, 0, 0, 20, NarratorChatListener.NO_TITLE) {
+        @Override
+        public void onPress() {
+            type = SyntaxHighlightingMode.values()[(type.ordinal() + 1) % SyntaxHighlightingMode.values().length];
+        }
+        
+        @Override
+        public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            setMessage(new TextComponent(type.toString()));
+            super.render(matrices, mouseX, mouseY, delta);
+        }
+    };
+    private final List<GuiEventListener> children = ImmutableList.of(buttonWidget);
+    
+    @SuppressWarnings("deprecation")
+    public SearchFilterSyntaxHighlightingEntry(Component fieldName, SyntaxHighlightingMode type, SyntaxHighlightingMode defaultValue, Consumer<SyntaxHighlightingMode> save) {
+        super(fieldName, null);
+        this.original = type;
+        this.type = type;
+        this.defaultValue = defaultValue;
+        this.save = save;
+    }
+    
+    @Override
+    public boolean isEdited() {
+        return super.isEdited() || getValue() != original;
+    }
+    
+    @Override
+    public SyntaxHighlightingMode getValue() {
+        return type;
+    }
+    
+    @Override
+    public Optional<SyntaxHighlightingMode> getDefaultValue() {
+        return Optional.ofNullable(defaultValue);
+    }
+    
+    @Override
+    public void save() {
+        save.accept(type);
+    }
+    
+    @Override
+    public List<? extends GuiEventListener> children() {
+        return children;
+    }
+    
+    @Override
+    public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isSelected, float delta) {
+        super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isSelected, delta);
+        Window window = Minecraft.getInstance().getWindow();
+        this.buttonWidget.active = this.isEditable();
+        this.buttonWidget.y = y;
+        Component displayedFieldName = this.getDisplayedFieldName();
+        if (Minecraft.getInstance().font.isBidirectional()) {
+            Minecraft.getInstance().font.drawShadow(matrices, displayedFieldName.getVisualOrderText(), window.getGuiScaledWidth() - x - Minecraft.getInstance().font.width(displayedFieldName), y + 6, 16777215);
+            this.buttonWidget.x = x;
+        } else {
+            Minecraft.getInstance().font.drawShadow(matrices, displayedFieldName.getVisualOrderText(), x, y + 6, this.getPreferredTextColor());
+            this.buttonWidget.x = x + entryWidth - 150;
+        }
+        
+        this.buttonWidget.setWidth(150);
+        this.buttonWidget.render(matrices, mouseX, mouseY, delta);
+    }
+}

+ 2 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/Menu.java

@@ -42,6 +42,7 @@ import me.shedaniel.rei.gui.modules.entries.SubSubsetsMenuEntry;
 import me.shedaniel.rei.gui.widget.LateRenderable;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.client.resources.language.I18n;
 import net.minecraft.core.Registry;
 import net.minecraft.world.item.CreativeModeTab;
@@ -262,7 +263,7 @@ public class Menu extends WidgetWithBounds implements LateRenderable {
     @Override
     public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
         if (getInnerBounds().contains(mouseX, mouseY)) {
-            scrolling.offset(ClothConfigInitializer.getScrollStep() * -amount, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -amount * (Screen.hasAltDown() ? 3 : 1), true);
             return true;
         }
         for (MenuEntry child : children()) {

+ 1 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -169,7 +169,7 @@ public class EntryListWidget extends WidgetWithBounds {
                     return true;
                 }
             } else if (ConfigObject.getInstance().isEntryListWidgetScrolled()) {
-                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3 * (Screen.hasAltDown() ? 3 : 1), true);
                 return true;
             }
         }

+ 1 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java

@@ -115,7 +115,7 @@ public class FavoritesListWidget extends WidgetWithBounds {
 //            if (favoritePanel.mouseScrolled(double_1, double_2, double_3)) {
 //                return true;
 //            }
-                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3 * (Screen.hasAltDown() ? 3 : 1), true);
                 return true;
             }
         }

+ 5 - 4
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java

@@ -59,10 +59,8 @@ import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.TransformingScreen;
 import me.shedaniel.rei.gui.WarningAndErrorScreen;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
-import me.shedaniel.rei.gui.config.entry.FilteringEntry;
-import me.shedaniel.rei.gui.config.entry.NoFilteringEntry;
-import me.shedaniel.rei.gui.config.entry.RecipeScreenTypeEntry;
-import me.shedaniel.rei.gui.config.entry.ReloadPluginsEntry;
+import me.shedaniel.rei.gui.config.SyntaxHighlightingMode;
+import me.shedaniel.rei.gui.config.entry.*;
 import me.shedaniel.rei.gui.credits.CreditsScreen;
 import me.shedaniel.rei.impl.filtering.FilteringRule;
 import me.shedaniel.rei.impl.filtering.rules.ManualFilteringRule;
@@ -175,6 +173,9 @@ public class ConfigManagerImpl implements ConfigManager {
         guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) ->
                         Collections.singletonList(new RecipeScreenTypeEntry(220, new TranslatableComponent(i13n), getUnsafely(field, config, RecipeScreenType.UNSET), getUnsafely(field, defaults), type -> setUnsafely(field, config, type)))
                 , (field) -> field.getType() == RecipeScreenType.class, ConfigObjectImpl.UseSpecialRecipeTypeScreen.class);
+        guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) ->
+                        Collections.singletonList(new SearchFilterSyntaxHighlightingEntry( new TranslatableComponent(i13n), getUnsafely(field, config, SyntaxHighlightingMode.COLORFUL), getUnsafely(field, defaults), type -> setUnsafely(field, config, type)))
+                , (field) -> field.getType() == SyntaxHighlightingMode.class, ConfigObjectImpl.UseSpecialSearchFilterSyntaxHighlightingScreen.class);
         guiRegistry.registerAnnotationProvider((i13n, field, config, defaults, guiProvider) ->
                         REIHelper.getInstance().getPreviousContainerScreen() == null || Minecraft.getInstance().getConnection() == null || Minecraft.getInstance().getConnection().getRecipeManager() == null ?
                                 Collections.singletonList(new NoFilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))

+ 10 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -361,6 +361,11 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return Mth.clamp(appearance.verticalEntriesBoundaries, 0.1,1);
     }
     
+    @Override
+    public SyntaxHighlightingMode getSyntaxHighlightingMode() {
+        return appearance.syntaxHighlightingMode;
+    }
+    
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD})
     @interface DontApplyFieldName {}
@@ -369,6 +374,10 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     @Target({ElementType.FIELD})
     @interface UseSpecialRecipeTypeScreen {}
     
+    @Retention(RetentionPolicy.RUNTIME)
+    @Target({ElementType.FIELD})
+    @interface UseSpecialSearchFilterSyntaxHighlightingScreen {}
+    
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD})
     @interface UseFilteringScreen {}
@@ -428,6 +437,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
         @UsePercentage(min = 0.1, max = 1.0, prefix = "Limit: ") private double horizontalEntriesBoundaries = 1.0;
         @UsePercentage(min = 0.1, max = 1.0, prefix = "Limit: ") private double verticalEntriesBoundaries = 1.0;
+        @UseSpecialSearchFilterSyntaxHighlightingScreen private SyntaxHighlightingMode syntaxHighlightingMode = SyntaxHighlightingMode.COLORFUL;
     }
     
     public static class Functionality {

+ 72 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/OverlaySearchFieldSyntaxHighlighter.java

@@ -0,0 +1,72 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.rei.gui.OverlaySearchField;
+import me.shedaniel.rei.impl.search.ArgumentsRegistry;
+import net.minecraft.util.IntRange;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+
+@ApiStatus.Internal
+public class OverlaySearchFieldSyntaxHighlighter implements Consumer<String> {
+    private final OverlaySearchField field;
+    public byte[] highlighted;
+    
+    public OverlaySearchFieldSyntaxHighlighter(OverlaySearchField field) {
+        this.field = field;
+        this.accept(field.getText());
+    }
+    
+    @Override
+    public void accept(String text) {
+        this.highlighted = new byte[text.length()];
+        SearchArgument.processSearchTerm(text, new SearchArgument.ProcessedSink() {
+            @Override
+            public void addQuote(int index) {
+                highlighted[index] = -2;
+            }
+            
+            @Override
+            public void addSplitter(int index) {
+                highlighted[index] = -1;
+            }
+            
+            @Override
+            public void addPart(SearchArgument<?, ?> argument, Collection<IntRange> grammarRanges, int index) {
+                int argIndex = ArgumentsRegistry.ARGUMENT_LIST.indexOf(argument.getArgument()) * 2 + 1;
+                for (int i = argument.start(); i < argument.end(); i++) {
+                    highlighted[i] = (byte) argIndex;
+                }
+                for (IntRange grammarRange : grammarRanges) {
+                    for (int i = grammarRange.getMinInclusive(); i <= grammarRange.getMaxInclusive(); i++) {
+                        highlighted[i + index] = (byte) (argIndex + 1);
+                    }
+                }
+            }
+        });
+    }
+}

+ 80 - 24
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/SearchArgument.java

@@ -37,9 +37,15 @@ import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.Minecraft;
 import net.minecraft.network.chat.Component;
+import net.minecraft.util.IntRange;
+import net.minecraft.util.Unit;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
@@ -47,38 +53,79 @@ import java.util.regex.Pattern;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public class SearchArgument {
+public class SearchArgument<T, R> {
     public static final String SPACE = " ", EMPTY = "";
-    private static final SearchArgument ALWAYS = new SearchArgument(AlwaysMatchingArgument.INSTANCE, EMPTY, true);
-    private Argument argument;
+    private static final SearchArgument<Unit, Unit> ALWAYS = new SearchArgument<>(AlwaysMatchingArgument.INSTANCE, EMPTY, true, -1, -1);
+    private Argument<T, R> argument;
     private String text;
-    private Object data;
+    private T filterData;
     private boolean regular;
+    private final int start;
+    private final int end;
     private static final Pattern SPLIT_PATTERN = Pattern.compile("(?:\"([^\"]*)\")|([^\\s]+)");
     
-    public SearchArgument(Argument argument, String text, boolean regular) {
-        this(argument, text, regular, true);
+    public SearchArgument(Argument<T, R> argument, String text, boolean regular, int start, int end) {
+        this(argument, text, regular, start, end, true);
     }
     
-    public SearchArgument(Argument argument, String text, boolean regular, boolean lowercase) {
+    public SearchArgument(Argument<T, R> argument, String text, boolean regular, int start, int end, boolean lowercase) {
         this.argument = argument;
         this.text = lowercase ? text.toLowerCase(Locale.ROOT) : text;
         this.regular = regular;
-        this.data = null;
+        this.filterData = null;
+        this.start = start;
+        this.end = end;
+    }
+    
+    public int start() {
+        return start;
+    }
+    
+    public int end() {
+        return end;
+    }
+    
+    public interface ProcessedSink {
+        void addQuote(int index);
+        
+        void addSplitter(int index);
+    
+        void addPart(SearchArgument<?, ?> argument, Collection<IntRange> grammarRanges, int index);
     }
     
     @ApiStatus.Internal
     public static List<SearchArgument.SearchArguments> processSearchTerm(String searchTerm) {
+        return processSearchTerm(searchTerm, null);
+    }
+    
+    @ApiStatus.Internal
+    public static List<SearchArgument.SearchArguments> processSearchTerm(String searchTerm, @Nullable ProcessedSink sink) {
         List<SearchArgument.SearchArguments> searchArguments = Lists.newArrayList();
-        for (String split : StringUtils.splitByWholeSeparatorPreserveAllTokens(searchTerm, "|")) {
-            Matcher terms = SPLIT_PATTERN.matcher(split);
-            List<SearchArgument> arguments = Lists.newArrayList();
+        int tokenStartIndex = 0;
+        String[] allTokens = StringUtils.splitByWholeSeparatorPreserveAllTokens(searchTerm, "|");
+        for (String token : allTokens) {
+            Matcher terms = SPLIT_PATTERN.matcher(token);
+            List<SearchArgument<?, ?>> arguments = Lists.newArrayList();
             while (terms.find()) {
                 String term = MoreObjects.firstNonNull(terms.group(1), terms.group(2));
-                for (Argument argument : ArgumentsRegistry.ARGUMENT_LIST) {
+                for (Argument<?, ?> argument : ArgumentsRegistry.ARGUMENT_LIST) {
                     MatchStatus status = argument.matchesArgumentPrefix(term);
                     if (status.isMatched()) {
-                        arguments.add(new SearchArgument(argument, status.getText(), !status.isInverted(), !status.shouldPreserveCasing()));
+                        SearchArgument<?, ?> searchArgument;
+                        if (terms.group(1) != null) {
+                            arguments.add(searchArgument = new SearchArgument<>(argument, status.getText(), !status.isInverted(), terms.start(1) + tokenStartIndex, terms.end(1) + tokenStartIndex, !status.shouldPreserveCasing()));
+                            if (sink != null) {
+                                sink.addQuote(terms.start() + tokenStartIndex);
+                                if (terms.end() - 1 + tokenStartIndex < searchTerm.length()) {
+                                    sink.addQuote(terms.end() - 1 + tokenStartIndex);
+                                }
+                            }
+                        } else {
+                            arguments.add(searchArgument = new SearchArgument<>(argument, status.getText(), !status.isInverted(), terms.start(2) + tokenStartIndex, terms.end(2) + tokenStartIndex, !status.shouldPreserveCasing()));
+                        }
+                        if (sink != null) {
+                            sink.addPart(searchArgument, status.grammarRanges(), terms.start() + tokenStartIndex);
+                        }
                         break;
                     }
                 }
@@ -88,10 +135,15 @@ public class SearchArgument {
             } else {
                 searchArguments.add(new SearchArgument.SearchArguments(arguments.toArray(new SearchArgument[0])));
             }
+            tokenStartIndex += 1 + token.length();
+            if (sink != null && tokenStartIndex - 1 < searchTerm.length()) {
+                sink.addSplitter(tokenStartIndex - 1);
+            }
         }
         for (SearchArguments arguments : searchArguments) {
-            for (SearchArgument argument : arguments.getArguments()) {
-                argument.data = argument.argument.prepareSearchData(argument.getText());
+            for (SearchArgument<?, ?> argument : arguments.getArguments()) {
+                //noinspection RedundantCast
+                ((SearchArgument<Object, Object>) argument).filterData = argument.argument.prepareSearchFilter(argument.getText());
             }
         }
         return searchArguments;
@@ -102,11 +154,12 @@ public class SearchArgument {
         if (searchArguments.isEmpty())
             return true;
         Minecraft minecraft = Minecraft.getInstance();
-        Object[] data = new Object[ArgumentsRegistry.ARGUMENT_LIST.size()];
+        Mutable<?> mutable = new MutableObject<>();
         for (SearchArgument.SearchArguments arguments : searchArguments) {
             boolean applicable = true;
-            for (SearchArgument argument : arguments.getArguments()) {
-                if (argument.getArgument().matches(data, stack, argument.getText(), argument.data) != argument.isRegular()) {
+            for (SearchArgument<?, ?> argument : arguments.getArguments()) {
+                mutable.setValue(null);
+                if (matches(argument.getArgument(), mutable, stack, argument.getText(), argument.filterData) != argument.isRegular()) {
                     applicable = false;
                     break;
                 }
@@ -117,6 +170,10 @@ public class SearchArgument {
         return false;
     }
     
+    private static <T, R, Z, B> boolean matches(Argument<T, B> argument, Mutable<Z> data, EntryStack stack, String filter, R filterData) {
+        return argument.matches((Mutable<B>) data, stack, filter, (T) filterData);
+    }
+    
     public static String tryGetEntryStackTooltip(EntryStack stack) {
         Tooltip tooltip = stack.getTooltip(new Point());
         if (tooltip != null)
@@ -124,7 +181,7 @@ public class SearchArgument {
         return "";
     }
     
-    public Argument getArgument() {
+    public Argument<?, ?> getArgument() {
         return argument;
     }
     
@@ -142,14 +199,14 @@ public class SearchArgument {
     }
     
     public static class SearchArguments {
-        public static final SearchArguments ALWAYS = new SearchArguments(new SearchArgument[]{SearchArgument.ALWAYS});
-        private SearchArgument[] arguments;
+        public static final SearchArguments ALWAYS = new SearchArguments(SearchArgument.ALWAYS);
+        private SearchArgument<?, ?>[] arguments;
         
-        public SearchArguments(SearchArgument[] arguments) {
+        public SearchArguments(SearchArgument<?, ?>... arguments) {
             this.arguments = arguments;
         }
         
-        public SearchArgument[] getArguments() {
+        public SearchArgument<?, ?>[] getArguments() {
             return arguments;
         }
         
@@ -157,5 +214,4 @@ public class SearchArgument {
             return this == ALWAYS;
         }
     }
-    
 }

+ 55 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/TextTransformations.java

@@ -26,6 +26,7 @@ package me.shedaniel.rei.impl;
 import me.shedaniel.math.Color;
 import net.minecraft.Util;
 import net.minecraft.client.Minecraft;
+import net.minecraft.network.chat.Style;
 import net.minecraft.network.chat.TextColor;
 import net.minecraft.util.FormattedCharSequence;
 import org.jetbrains.annotations.ApiStatus;
@@ -41,4 +42,58 @@ public class TextTransformations {
             return sink.accept(charIndex, style.withColor(TextColor.fromRgb(rgb)), codePoint);
         });
     }
+    
+    public static FormattedCharSequence forwardWithTransformation(String text, CharSequenceTransformer transformer) {
+        if (text.isEmpty()) {
+            return FormattedCharSequence.EMPTY;
+        }
+        return sink -> {
+            int length = text.length();
+            
+            for (int charIndex = 0; charIndex < length; ++charIndex) {
+                char c = text.charAt(charIndex);
+                
+                if (Character.isHighSurrogate(c)) {
+                    if (charIndex + 1 >= length) {
+                        // Broken?
+                        if (!sink.accept(charIndex, Style.EMPTY, 65533)) {
+                            return false;
+                        }
+                        break;
+                    }
+                    
+                    char forward = text.charAt(charIndex + 1);
+                    if (Character.isLowSurrogate(forward)) {
+                        // Combine them together
+                        if (!sink.accept(charIndex, Style.EMPTY, Character.toCodePoint(c, forward))) {
+                            return false;
+                        }
+                        
+                        charIndex++;
+                    } else {
+                        // Broken?
+                        if (!sink.accept(charIndex, Style.EMPTY, 65533)) {
+                            return false;
+                        }
+                    }
+                } else if (Character.isSurrogate(c)) {
+                    // This is weird, broken?
+                    if (!sink.accept(charIndex, Style.EMPTY, 65533)) {
+                        return false;
+                    }
+                } else {
+                    if (!sink.accept(charIndex, transformer.apply(text, charIndex, c), c)) {
+                        return false;
+                    }
+                }
+            }
+            
+            return true;
+        };
+    }
+    
+    @FunctionalInterface
+    public interface CharSequenceTransformer {
+        Style apply(String text, int charIndex, char c);
+    }
 }

+ 9 - 2
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/AlwaysMatchingArgument.java

@@ -26,11 +26,13 @@ package me.shedaniel.rei.impl.search;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.util.Unit;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class AlwaysMatchingArgument extends Argument {
+public final class AlwaysMatchingArgument extends Argument<Unit, Unit> {
     public static final AlwaysMatchingArgument INSTANCE = new AlwaysMatchingArgument();
     
     @Override
@@ -39,10 +41,15 @@ public final class AlwaysMatchingArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
+    public boolean matches(Mutable<Unit> data, EntryStack stack, String searchText, Unit filterData) {
         return true;
     }
     
+    @Override
+    public Unit prepareSearchFilter(String searchText) {
+        return Unit.INSTANCE;
+    }
+    
     @Override
     public MatchStatus matchesArgumentPrefix(String text) {
         return MatchStatus.unmatched();

+ 15 - 17
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/Argument.java

@@ -26,17 +26,18 @@ package me.shedaniel.rei.impl.search;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.network.chat.Style;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public abstract class Argument {
+public abstract class Argument<T, R> {
     public Argument() {
     }
     
-    private int dataOrdinal = -1;
-    
     public abstract String getName();
     
     @Nullable
@@ -44,24 +45,21 @@ public abstract class Argument {
         return null;
     }
     
+    @NotNull
+    public Style getHighlightedStyle() {
+        return Style.EMPTY;
+    }
+    
     public MatchStatus matchesArgumentPrefix(String text) {
         String prefix = getPrefix();
         if (prefix == null) return MatchStatus.unmatched();
-        if (text.startsWith("-" + prefix)) return MatchStatus.invertMatched(text.substring(1 + prefix.length()));
-        if (text.startsWith(prefix + "-")) return MatchStatus.invertMatched(text.substring(1 + prefix.length()));
-        return text.startsWith(prefix) ? MatchStatus.matched(text.substring(prefix.length())) : MatchStatus.unmatched();
+        if (text.startsWith("-" + prefix)) return MatchStatus.invertMatched(text.substring(1 + prefix.length())).grammar(0, prefix.length() + 1);
+        if (text.startsWith(prefix + "-")) return MatchStatus.invertMatched(text.substring(1 + prefix.length())).grammar(0, prefix.length() + 1);
+        if (text.startsWith(prefix)) return MatchStatus.matched(text.substring(prefix.length())).grammar(0, prefix.length());
+        return MatchStatus.unmatched();
     }
     
-    public final int getDataOrdinal() {
-        if (dataOrdinal == -1) {
-            dataOrdinal = ArgumentsRegistry.ARGUMENT_LIST.indexOf(this);
-        }
-        return dataOrdinal;
-    }
+    public abstract boolean matches(Mutable<R> data, EntryStack stack, String searchText, T filterData);
     
-    public abstract boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData);
-    
-    public Object prepareSearchData(String searchText) {
-        return null;
-    }
+    public abstract T prepareSearchFilter(String searchText);
 }

+ 3 - 3
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/ArgumentsRegistry.java

@@ -32,8 +32,8 @@ import java.util.Map;
 
 @ApiStatus.Internal
 public final class ArgumentsRegistry {
-    public static final Map<String, Argument> ARGUMENTS = Maps.newHashMap();
-    public static final List<Argument> ARGUMENT_LIST = Lists.newArrayList();
+    public static final Map<String, Argument<?, ?>> ARGUMENTS = Maps.newHashMap();
+    public static final List<Argument<?, ?>> ARGUMENT_LIST = Lists.newArrayList();
     
     static {
         register(AlwaysMatchingArgument.INSTANCE);
@@ -44,7 +44,7 @@ public final class ArgumentsRegistry {
         register(TextArgument.INSTANCE);
     }
     
-    private static void register(Argument argument) {
+    private static void register(Argument<?, ?> argument) {
         ARGUMENTS.put(argument.getName(), argument);
         ARGUMENT_LIST.add(argument);
     }

+ 29 - 3
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/MatchStatus.java

@@ -23,19 +23,23 @@
 
 package me.shedaniel.rei.impl.search;
 
+import net.minecraft.util.IntRange;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 @ApiStatus.Internal
 public final class MatchStatus {
     private static final MatchStatus UNMATCHED = new MatchStatus(MatchType.UNMATCHED, null, false);
-    private final MatchType type;
+    private MatchType type;
     @Nullable
     private final String text;
     private final boolean preserveCasing;
+    private final List<IntRange> grammarRanges = new ArrayList<>();
     
     private MatchStatus(MatchType type, @Nullable String text, boolean preserveCasing) {
         this.type = type;
@@ -48,11 +52,11 @@ public final class MatchStatus {
     }
     
     public static MatchStatus invertMatched(@NotNull String text) {
-        return invertMatched(text, false);
+        return matched(text, false).invert();
     }
     
     public static MatchStatus invertMatched(@NotNull String text, boolean preserveCasing) {
-        return new MatchStatus(MatchType.INVERT_MATCHED, Objects.requireNonNull(text), preserveCasing);
+        return matched(Objects.requireNonNull(text), preserveCasing).invert();
     }
     
     public static MatchStatus matched(@NotNull String text) {
@@ -63,6 +67,28 @@ public final class MatchStatus {
         return new MatchStatus(MatchType.MATCHED, Objects.requireNonNull(text), preserveCasing);
     }
     
+    public static MatchStatus result(@NotNull String text, boolean preserveCasing, boolean inverted) {
+        return new MatchStatus(!inverted ? MatchType.MATCHED : MatchType.INVERT_MATCHED, Objects.requireNonNull(text), preserveCasing);
+    }
+    
+    public List<IntRange> grammarRanges() {
+        return grammarRanges;
+    }
+    
+    public MatchStatus grammar(int start, int end) {
+        if (end - 1 >= start) {
+            this.grammarRanges.add(IntRange.of(start,  end - 1));
+        }
+        return this;
+    }
+    
+    public MatchStatus invert() {
+        if (isMatched()) {
+            this.type = isInverted() ? MatchType.MATCHED : MatchType.INVERT_MATCHED;
+        }
+        return this;
+    }
+    
     public boolean isMatched() {
         return type != MatchType.UNMATCHED;
     }

+ 42 - 11
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/ModArgument.java

@@ -27,16 +27,23 @@ import me.shedaniel.rei.api.ClientHelper;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.network.chat.Style;
+import net.minecraft.network.chat.TextColor;
 import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Unit;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Locale;
+import java.util.Optional;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class ModArgument extends Argument {
+public final class ModArgument extends Argument<Unit, ModArgument.@Nullable ModInfoPair> {
     public static final ModArgument INSTANCE = new ModArgument();
+    private static final Style STYLE = Style.EMPTY.withColor(TextColor.fromRgb(0xffa8f3));
     
     @Override
     public String getName() {
@@ -49,21 +56,45 @@ public final class ModArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
-        if (data[getDataOrdinal()] == null) {
-            data[getDataOrdinal()] = new String[]{
-                    stack.getIdentifier().map(ResourceLocation::getNamespace).orElse("").toLowerCase(Locale.ROOT),
+    public boolean matches(Mutable<@Nullable ModInfoPair> data, EntryStack stack, String searchText, Unit filterData) {
+        if (data.getValue() == null) {
+            Optional<ResourceLocation> id = stack.getIdentifier();
+            data.setValue(id.isPresent() ? new ModInfoPair(
+                    id.get().getNamespace(),
                     null
-            };
+            ) : ModInfoPair.EMPTY);
         }
-        String[] strings = (String[]) data[getDataOrdinal()];
-        if (strings[0].isEmpty() || strings[0].contains(searchText)) return true;
-        if (strings[1] == null) {
-            strings[1] = ClientHelper.getInstance().getModFromModId(strings[0]).toLowerCase(Locale.ROOT);
+        ModInfoPair pair = data.getValue();
+        if (pair.modId == null || pair.modId.contains(searchText)) return true;
+        if (pair.modName == null) {
+            pair.modName = ClientHelper.getInstance().getModFromModId(pair.modId).toLowerCase(Locale.ROOT);
         }
-        return strings[1].isEmpty() || strings[1].contains(searchText);
+        return pair.modName.isEmpty() || pair.modName.contains(searchText);
+    }
+    
+    @Override
+    public Unit prepareSearchFilter(String searchText) {
+        return Unit.INSTANCE;
+    }
+    
+    @Override
+    public @NotNull Style getHighlightedStyle() {
+        return STYLE;
     }
     
     private ModArgument() {
     }
+    
+    protected static class ModInfoPair {
+        private static final ModInfoPair EMPTY = new ModInfoPair(null, null);
+        @Nullable
+        private final String modId;
+        @Nullable
+        private String modName;
+        
+        public ModInfoPair(@Nullable String modId, @Nullable String modName) {
+            this.modId = modId;
+            this.modName = modName;
+        }
+    }
 }

+ 22 - 10
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/RegexArgument.java

@@ -26,7 +26,12 @@ package me.shedaniel.rei.impl.search;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.network.chat.Style;
+import net.minecraft.network.chat.TextColor;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -34,8 +39,9 @@ import java.util.regex.PatternSyntaxException;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class RegexArgument extends Argument {
+public final class RegexArgument extends Argument<@Nullable Pattern, String> {
     public static final RegexArgument INSTANCE = new RegexArgument();
+    private static final Style STYLE = Style.EMPTY.withColor(TextColor.fromRgb(0xbfffa8));
     
     @Override
     public String getName() {
@@ -51,12 +57,15 @@ public final class RegexArgument extends Argument {
             matchText = matchText.substring(1);
         }
         if (matchText.length() >= 3 && matchText.startsWith("r/") && matchText.endsWith("/"))
-            return !inverted ? MatchStatus.matched(matchText.substring(2, matchText.length() - 1), true) : MatchStatus.invertMatched(matchText.substring(2, matchText.length() - 1), true);
+            return MatchStatus.result(matchText.substring(2, matchText.length() - 1), true, inverted)
+                    .grammar(0, inverted ? 3 : 2)
+                    .grammar(text.length() - 1, text.length());
         return MatchStatus.unmatched();
     }
     
     @Override
-    public Object prepareSearchData(String searchText) {
+    @Nullable
+    public Pattern prepareSearchFilter(String searchText) {
         try {
             return Pattern.compile(searchText);
         } catch (PatternSyntaxException ignored) {
@@ -65,14 +74,17 @@ public final class RegexArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
-        Pattern pattern = (Pattern) searchData;
-        if (pattern == null) return false;
-        if (data[getDataOrdinal()] == null) {
-            String name = stack.asFormatStrippedText().getString();
-            data[getDataOrdinal()] = name;
+    public @NotNull Style getHighlightedStyle() {
+        return STYLE;
+    }
+    
+    @Override
+    public boolean matches(Mutable<String> data, EntryStack stack, String searchText, @Nullable Pattern filterData) {
+        if (filterData == null) return false;
+        if (data.getValue() == null) {
+            data.setValue(stack.asFormatStrippedText().getString());
         }
-        Matcher matcher = pattern.matcher((String) data[getDataOrdinal()]);
+        Matcher matcher = filterData.matcher(data.getValue());
         return matcher != null && matcher.matches();
     }
     

+ 37 - 19
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TagArgument.java

@@ -27,17 +27,25 @@ import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.Minecraft;
+import net.minecraft.network.chat.Style;
+import net.minecraft.network.chat.TextColor;
 import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.Unit;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collection;
+import java.util.Collections;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class TagArgument extends Argument {
+public final class TagArgument extends Argument<Unit, String[]> {
     public static final TagArgument INSTANCE = new TagArgument();
     private static final Minecraft minecraft = Minecraft.getInstance();
+    private static final String[] EMPTY_ARRAY = new String[0];
+    private static final Style STYLE = Style.EMPTY.withColor(TextColor.fromRgb(0x9efff4));
     
     @Override
     public String getName() {
@@ -50,36 +58,46 @@ public final class TagArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
-        if (data[getDataOrdinal()] == null) {
+    public @NotNull Style getHighlightedStyle() {
+        return STYLE;
+    }
+    
+    @Override
+    public boolean matches(Mutable<String[]> data, EntryStack stack, String searchText, Unit filterData) {
+        if (data.getValue() == null) {
+            Collection<ResourceLocation> tags = Collections.emptyList();
             if (stack.getType() == EntryStack.Type.ITEM) {
-                Collection<ResourceLocation> tagsFor = minecraft.getConnection().getTags().getItems().getMatchingTags(stack.getItem());
-                data[getDataOrdinal()] = new String[tagsFor.size()];
-                int i = 0;
+                tags = minecraft.getConnection().getTags().getItems().getMatchingTags(stack.getItem());
                 
-                for (ResourceLocation identifier : tagsFor) {
-                    ((String[]) data[getDataOrdinal()])[i] = identifier.toString();
-                    i++;
-                }
             } else if (stack.getType() == EntryStack.Type.FLUID) {
-                Collection<ResourceLocation> tagsFor = minecraft.getConnection().getTags().getFluids().getMatchingTags(stack.getFluid());
-                data[getDataOrdinal()] = new String[tagsFor.size()];
+                tags = minecraft.getConnection().getTags().getFluids().getMatchingTags(stack.getFluid());
+            }
+            
+            if (tags.isEmpty()) {
+                data.setValue(EMPTY_ARRAY);
+            } else {
+                data.setValue(new String[tags.size()]);
                 int i = 0;
                 
-                for (ResourceLocation identifier : tagsFor) {
-                    ((String[]) data[getDataOrdinal()])[i] = identifier.toString();
+                for (ResourceLocation identifier : tags) {
+                    data.getValue()[i] = identifier.toString();
                     i++;
                 }
-            } else
-                data[getDataOrdinal()] = new String[0];
+            }
         }
-        String[] tags = (String[]) data[getDataOrdinal()];
-        for (String tag : tags)
-            if (tag.isEmpty() || tag.contains(searchText))
+        for (String tag : data.getValue()) {
+            if (tag.isEmpty() || tag.contains(searchText)) {
                 return true;
+            }
+        }
         return false;
     }
     
+    @Override
+    public Unit prepareSearchFilter(String searchText) {
+        return Unit.INSTANCE;
+    }
+    
     private TagArgument() {
     }
 }

+ 12 - 5
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TextArgument.java

@@ -26,6 +26,8 @@ package me.shedaniel.rei.impl.search;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.util.Unit;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.Nullable;
 
@@ -33,7 +35,7 @@ import java.util.Locale;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class TextArgument extends Argument {
+public final class TextArgument extends Argument<Unit, String> {
     public static final TextArgument INSTANCE = new TextArgument();
     
     @Override
@@ -47,11 +49,16 @@ public final class TextArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
-        if (data[getDataOrdinal()] == null) {
-            data[getDataOrdinal()] = stack.asFormatStrippedText().getString().toLowerCase(Locale.ROOT);
+    public boolean matches(Mutable<String> data, EntryStack stack, String searchText, Unit filterData) {
+        if (data.getValue() == null) {
+            data.setValue(stack.asFormatStrippedText().getString().toLowerCase(Locale.ROOT));
         }
-        return ((String) data[getDataOrdinal()]).contains(searchText);
+        return data.getValue().contains(searchText);
+    }
+    
+    @Override
+    public Unit prepareSearchFilter(String searchText) {
+        return null;
     }
     
     private TextArgument() {

+ 21 - 5
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/search/TooltipArgument.java

@@ -27,15 +27,21 @@ import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.impl.SearchArgument;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.network.chat.Style;
+import net.minecraft.network.chat.TextColor;
+import net.minecraft.util.Unit;
+import org.apache.commons.lang3.mutable.Mutable;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Locale;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
-public final class TooltipArgument extends Argument {
+public final class TooltipArgument extends Argument<Unit, String> {
     public static final TooltipArgument INSTANCE = new TooltipArgument();
+    private static final Style STYLE = Style.EMPTY.withColor(TextColor.fromRgb(0xffe0ad));
     
     @Override
     public String getName() {
@@ -48,14 +54,24 @@ public final class TooltipArgument extends Argument {
     }
     
     @Override
-    public boolean matches(Object[] data, EntryStack stack, String searchText, Object searchData) {
-        if (data[getDataOrdinal()] == null) {
-            data[getDataOrdinal()] = SearchArgument.tryGetEntryStackTooltip(stack).toLowerCase(Locale.ROOT);
+    public @NotNull Style getHighlightedStyle() {
+        return STYLE;
+    }
+    
+    @Override
+    public boolean matches(Mutable<String> data, EntryStack stack, String searchText, Unit filterData) {
+        if (data.getValue() == null) {
+            data.setValue(SearchArgument.tryGetEntryStackTooltip(stack).toLowerCase(Locale.ROOT));
         }
-        String tooltip = (String) data[getDataOrdinal()];
+        String tooltip = data.getValue();
         return tooltip.isEmpty() || tooltip.contains(searchText);
     }
     
+    @Override
+    public Unit prepareSearchFilter(String searchText) {
+        return Unit.INSTANCE;
+    }
+    
     private TooltipArgument() {
     }
 }

+ 1 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java

@@ -66,7 +66,7 @@ public final class ArrowWidget extends Arrow {
     @Override
     public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
         Minecraft.getInstance().getTextureManager().bind(REIHelper.getInstance().getDefaultDisplayTexture());
-        blit(matrices, getX(), getY(), 106, 91, 24, 17);
+        blit(matrices, getX(), getY(), 82, 60, 24, 17);
         if (getAnimationDuration() > 0) {
             int width = Mth.ceil((System.currentTimeMillis() / (animationDuration / 24) % 24d) / 1f);
             blit(matrices, getX(), getY(), 82, 91, width, 17);

BIN
RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/display.png


BIN
RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/display_dark.png


BIN
RoughlyEnoughItems-runtime/src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer_dark.png


+ 1 - 1
gradle.properties

@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx3G
-mod_version=5.8.12
+mod_version=5.9.0
 supported_version=1.16.2/3/4/5
 minecraft_version=1.16.4
 fabricloader_version=0.10.6+build.214

+ 6 - 0
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -210,6 +210,12 @@
   "config.roughlyenoughitems.scrollingEntryListWidget.boolean.false": "Paginated",
   "config.roughlyenoughitems.horizontalEntriesBoundaries": "Horizontal Entries Boundaries:",
   "config.roughlyenoughitems.verticalEntriesBoundaries": "Vertical Entries Boundaries:",
+  "config.roughlyenoughitems.syntaxHighlightingMode": "Syntax Highlighting Mode:",
+  "config.roughlyenoughitems.syntaxHighlightingMode.config": "%s",
+  "config.roughlyenoughitems.syntaxHighlightingMode.plain": "Plain",
+  "config.roughlyenoughitems.syntaxHighlightingMode.plain_underscored": "Plain (Underscored)",
+  "config.roughlyenoughitems.syntaxHighlightingMode.colorful": "Colorful",
+  "config.roughlyenoughitems.syntaxHighlightingMode.colorful_underscored": "Colorful (Underscored)",
   "config.roughlyenoughitems.filteringScreen": "Customized Filtering",
   "config.roughlyenoughitems.filteringRulesScreen": "Customized Filtering Rules",
   "config.roughlyenoughitems.filteringRulesScreen.new": "Create Filtering Rule",