package io.gitlab.jfronny.libjf.config.impl.client.screen; import io.gitlab.jfronny.libjf.LibJf; import io.gitlab.jfronny.libjf.config.api.ConfigInstance; import io.gitlab.jfronny.libjf.config.api.Entry; import io.gitlab.jfronny.libjf.config.impl.EntryInfo; import io.gitlab.jfronny.libjf.gson.GsonHidden; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.LiteralText; import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; import net.minecraft.util.Formatting; import java.util.AbstractMap; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; @Environment(EnvType.CLIENT) public class EntryInfoWidgetBuilder { private static final Pattern INTEGER_ONLY = Pattern.compile("(-?[0-9]*)"); private static final Pattern DECIMAL_ONLY = Pattern.compile("-?([\\d]+\\.?[\\d]*|[\\d]*\\.?[\\d]+|\\.)"); public static void initConfig(ConfigInstance config) { for (EntryInfo info : config.getEntries()) { if (info.field.isAnnotationPresent(Entry.class) || info.field.isAnnotationPresent(GsonHidden.class)) try { initEntry(config, info); } catch (Exception ignored) { } } for (ConfigInstance value : config.getCategories().values()) { initConfig(value); } } private static void initEntry(ConfigInstance config, EntryInfo info) { if (!(info.field.isAnnotationPresent(io.gitlab.jfronny.libjf.config.api.Entry.class) || info.field.isAnnotationPresent(GsonHidden.class))) return; Class type = info.field.getType(); info.width = info.entry != null ? info.entry.width() : 0; if (info.entry == null) return; if (type == int.class || type == Integer.class) textField(config, info, INTEGER_ONLY, Integer::parseInt, true, info.entry.min(), info.entry.max()); else if (type == float.class || type == Float.class) textField(config, info, DECIMAL_ONLY, Float::parseFloat, false, info.entry.min(), info.entry.max()); else if (type == double.class || type == Double.class) textField(config, info, DECIMAL_ONLY, Double::parseDouble, false, info.entry.min(), info.entry.max()); else if (type == String.class) textField(config, info, null, String::length, true, Math.min(info.entry.min(),0), Math.max(info.entry.max(),1)); else if (type == boolean.class || type == Boolean.class) { Function func = value -> new LiteralText((Boolean) value ? "True" : "False").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED); toggle(info, button -> { info.value = !(Boolean) info.value; button.setMessage(func.apply(info.value)); }, func); } else if (type.isEnum()) { List values = Arrays.asList(info.field.getType().getEnumConstants()); Function func = value -> new TranslatableText(config.getModId() + ".jfconfig.enum." + type.getSimpleName() + "." + info.value.toString()); toggle(info, button -> { int index = values.indexOf(info.value) + 1; info.value = values.get(index >= values.size() ? 0 : index); button.setMessage(func.apply(info.value)); }, func); } else LibJf.LOGGER.error("Invalid entry type in " + info.field.getName() + ": " + type.getName()); try { info.value = info.field.get(null); info.tempValue = info.value.toString(); } catch (IllegalAccessException ignored) { } } private static void toggle(EntryInfo info, ButtonWidget.PressAction pressAction, Function valueTextifier) { info.widget = (width, textRenderer, done) -> new ButtonWidget(width - 110, 0, info.width, 20, valueTextifier.apply(info.value), pressAction); } /** * @param config The config this entry is a part of * @param info The entry to generate a widget for * @param pattern The pattern a valid value must abide to * @param sizeFetcher A function to get a number for size constraints * @param wholeNumber Whether size constraints are whole numbers * @param min The minimum size of a valid value * @param max The maximum size of a valid value */ private static void textField(ConfigInstance config, EntryInfo info, Pattern pattern, Function sizeFetcher, boolean wholeNumber, double min, double max) { boolean isNumber = pattern != null; info.widget = (width, textRenderer, done) -> { TextFieldWidget widget = new TextFieldWidget(textRenderer, width - 110, 0, info.width, 20, null); widget.setText(info.tempValue); Predicate processor = s -> { s = s.trim(); if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches())) return false; Number value = 0; boolean inLimits = false; info.error = null; if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) { value = sizeFetcher.apply(s); inLimits = value.doubleValue() >= min && value.doubleValue() <= max; info.error = inLimits? null : new AbstractMap.SimpleEntry<>(widget, new LiteralText(value.doubleValue() < min ? "§cMinimum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) min : " is " + min) : "§cMaximum " + (isNumber? "value" : "length") + (wholeNumber ? " is " + (int) max : " is " + max))); } info.tempValue = s; widget.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); info.inLimits = inLimits; done.active = config.getEntries().stream().allMatch(e -> e.inLimits); if (inLimits) info.value = isNumber? value : s; return true; }; widget.setTextPredicate(processor); return widget; }; } }