package io.gitlab.jfronny.libjf.config.impl.client.gui; import io.gitlab.jfronny.commons.throwable.Try; import io.gitlab.jfronny.commons.tuple.Tuple; 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.api.EntryInfo; import io.gitlab.jfronny.libjf.config.api.WidgetFactory; import io.gitlab.jfronny.libjf.config.impl.EntryInfoImpl; 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.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; import java.util.regex.Pattern; @Environment(EnvType.CLIENT) public class EntryInfoWidgetBuilder { private static final Pattern INTEGER_ONLY = Pattern.compile("(-?\\d*)"); private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)"); public static List> buildWidgets(ConfigInstance config) { WidgetState state; List> knownStates = new ArrayList<>(); for (EntryInfo info : config.getEntries()) { if ((state = initEntry(config, info, knownStates)) != null) { knownStates.add(state); } } return knownStates; } private static WidgetState initEntry(ConfigInstance config, EntryInfo info, List> knownStates) { Class type = info.getValueType(); WidgetState state = new WidgetState<>(); WidgetFactory factory; if (type == int.class || type == Integer.class) factory = textField(info, state, INTEGER_ONLY, Integer::parseInt, true, info.getMinValue(), info.getMaxValue()); else if (type == float.class || type == Float.class) factory = textField(info, state, DECIMAL_ONLY, Float::parseFloat, false, info.getMinValue(), info.getMaxValue()); else if (type == double.class || type == Double.class) factory = textField(info, state, DECIMAL_ONLY, Double::parseDouble, false, info.getMinValue(), info.getMaxValue()); else if (type == String.class) factory = textField(info, state, null, String::length, true, Math.min(info.getMinValue(),0), Math.max(info.getMaxValue(),1)); else if (type == boolean.class || type == Boolean.class) { factory = toggle(info, state, value -> !(Boolean) value, value -> new LiteralText((Boolean) value ? "True" : "False").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED)); } else if (type.isEnum()) { List values = Arrays.asList(info.getValueType().getEnumConstants()); factory = toggle(info, state, value -> { int index = values.indexOf(value) + 1; return values.get(index >= values.size() ? 0 : index); }, value -> new TranslatableText(config.getModId() + ".jfconfig.enum." + type.getSimpleName() + "." + state.cachedValue)); } else { LibJf.LOGGER.error("Invalid entry type in " + info.getName() + ": " + type.getName()); factory = ((screenWidth, textRenderer, done) -> new WidgetFactory.Widget(() -> {}, new ButtonWidget(-10, 0, 0, 0, Text.of(""), null))); } Try.orThrow(() -> state.initialize(info, knownStates, factory)); return state; } private static WidgetFactory toggle(EntryInfo info, WidgetState state, Function increment, Function valueTextifier) { return (screenWidth, textRenderer, done) -> { ButtonWidget button = new ButtonWidget(screenWidth - 110, 0, info.getWidth(), 20, valueTextifier.apply(state.cachedValue), btn -> { state.updateCache((T) increment.apply(state.cachedValue)); btn.setMessage(valueTextifier.apply(state.cachedValue)); }); return new WidgetFactory.Widget(() -> button.setMessage(valueTextifier.apply(state.cachedValue)), button); }; } /** * @param info The entry to generate a widget for * @param state The state representation of this widget * @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 WidgetFactory textField(EntryInfo info, WidgetState state, Pattern pattern, Function sizeFetcher, boolean wholeNumber, double min, double max) { boolean isNumber = pattern != null; return (width, textRenderer, done) -> { TextFieldWidget widget = new TextFieldWidget(textRenderer, width - 110, 0, info.getWidth(), 20, null); widget.setText(state.tempValue); widget.setTextPredicate(currentInput -> { currentInput = currentInput.trim(); if (!(currentInput.isEmpty() || !isNumber || pattern.matcher(currentInput).matches())) return false; Number value = 0; boolean inLimits = false; state.error = null; if (!(isNumber && currentInput.isEmpty()) && !currentInput.equals("-") && !currentInput.equals(".")) { value = sizeFetcher.apply(currentInput); inLimits = value.doubleValue() >= min && value.doubleValue() <= max; state.error = inLimits ? null : Tuple.of(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))); } state.tempValue = currentInput; widget.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); state.inLimits = inLimits; done.active = state.knownStates.stream().allMatch(st -> st.inLimits); if (inLimits) { //Coerce.consumer(info::setValue).addHandler(e -> {}).accept(isNumber ? (T) value : (T) currentInput); state.cachedValue = isNumber ? (T) value : (T) currentInput; } return true; }); return new WidgetFactory.Widget(() -> widget.setText(state.cachedValue == null ? "" : state.cachedValue.toString()), widget); }; } }