package wtf.hak.survivalfabric.modmenu; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; import net.minecraft.util.math.MathHelper; import wtf.hak.survivalfabric.config.client.ClientConfig; import wtf.hak.survivalfabric.config.client.ClientConfigManager; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; public class ConfigScreen extends Screen { private static final int OPTION_HEIGHT = 25; private static final int SCROLL_BAR_WIDTH = 6; private static final int TOP_PADDING = 40; private static final int BOTTOM_PADDING = 35; private static final int SIDE_PADDING = 20; private final Screen parent; private final List> options = new ArrayList<>(); private TextFieldWidget activeTextField = null; private float scrollPosition = 0.0F; private boolean scrolling = false; private int contentHeight = 0; public ConfigScreen(Screen parent) { super(Text.literal("Survival Fabric - Client Config")); this.parent = parent; } @Override protected void init() { options.clear(); int listWidth = this.width - (SIDE_PADDING * 2); int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; int listLeft = SIDE_PADDING; int listTop = TOP_PADDING; int index = 0; for (Field field : ClientConfig.class.getFields()) { try { Class type = field.getType(); String name = formatFieldName(field.getName()); ConfigOption option = null; if (type == boolean.class) { boolean value = field.getBoolean(ClientConfigManager.getConfig()); option = new BooleanConfigOption( name, field, value, listLeft, listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, listWidth ); } else if (type == int.class) { int value = field.getInt(ClientConfigManager.getConfig()); option = new IntegerConfigOption( name, field, value, listLeft, listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, listWidth ); } else if (type == float.class) { float value = field.getFloat(ClientConfigManager.getConfig()); option = new FloatConfigOption( name, field, value, listLeft, listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, listWidth ); } else if (type == String.class) { String value = (String) field.get(ClientConfigManager.getConfig()); option = new StringConfigOption( name, field, value, listLeft, listTop + (index * OPTION_HEIGHT) - (int) scrollPosition, listWidth ); } if (option != null) { options.add(option); index++; } } catch (Exception e) { e.printStackTrace(); } } for (ConfigOption option : options) { if (option instanceof NumericConfigOption) { ((NumericConfigOption) option).createTextField(this.client); } } contentHeight = options.size() * OPTION_HEIGHT; this.addDrawableChild(ButtonWidget.builder( Text.translatable("gui.done"), button -> this.client.setScreen(parent) ).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { this.renderBackground(context, mouseX, mouseY, delta); int titleX = (this.width / 2) - (this.textRenderer.getWidth(this.title) / 2); context.drawText(this.textRenderer, this.title, titleX, 15, 0xFFFFFF, true); int listWidth = this.width - (SIDE_PADDING * 2); int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; int listLeft = SIDE_PADDING; int listTop = TOP_PADDING; context.drawBorder(listLeft, listTop, listLeft + listWidth, listTop + listHeight, 0x00000000); context.enableScissor( listLeft, listTop, listLeft + listWidth, listTop + listHeight ); for (int i = 0; i < options.size(); i++) { ConfigOption option = options.get(i); option.y = listTop + (i * OPTION_HEIGHT) - (int) scrollPosition; if (option instanceof NumericConfigOption) { ((NumericConfigOption) option).updateTextFieldPosition(); } if (option.y < listTop + listHeight && option.y + OPTION_HEIGHT > listTop) { option.render(context, mouseX, mouseY, delta); } } context.disableScissor(); if (contentHeight > listHeight) { int scrollBarHeight = Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight)); int scrollBarY = listTop + (int) ((scrollPosition / (contentHeight - listHeight)) * (listHeight - scrollBarHeight)); context.fill( listLeft + listWidth + 2, listTop, listLeft + listWidth + 2 + SCROLL_BAR_WIDTH, listTop + listHeight, 0xFF404040 ); context.fill( listLeft + listWidth + 2, scrollBarY, listLeft + listWidth + 2 + SCROLL_BAR_WIDTH, scrollBarY + scrollBarHeight, scrolling ? 0xFFAAAAAA : 0xFF808080 ); } } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { int listWidth = this.width - (SIDE_PADDING * 2); int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; int listLeft = SIDE_PADDING; int listTop = TOP_PADDING; boolean handledByTextField = false; activeTextField = null; if (mouseX >= listLeft && mouseX <= listLeft + listWidth && mouseY >= listTop && mouseY <= listTop + listHeight) { for (ConfigOption option : options) { if (option instanceof NumericConfigOption numOption && option.y >= listTop && option.y + OPTION_HEIGHT <= listTop + listHeight) { TextFieldWidget textField = numOption.getTextField(); if (textField.isMouseOver(mouseX, mouseY)) { textField.setFocused(true); activeTextField = textField; handledByTextField = true; for (ConfigOption otherOption : options) { if (otherOption instanceof NumericConfigOption && otherOption != option) { ((NumericConfigOption) otherOption).getTextField().setFocused(false); } } return true; } else { textField.setFocused(false); } } } } if (contentHeight > listHeight && mouseX >= listLeft + listWidth + 2 && mouseX <= listLeft + listWidth + 2 + SCROLL_BAR_WIDTH && mouseY >= listTop && mouseY <= listTop + listHeight) { scrolling = true; return true; } // Check if clicked on an option if (mouseX >= listLeft && mouseX <= listLeft + listWidth && mouseY >= listTop && mouseY <= listTop + listHeight) { for (ConfigOption option : options) { if (option.isMouseOver(mouseX, mouseY) && option.y >= listTop && option.y + OPTION_HEIGHT <= listTop + listHeight) { option.onClick(mouseX, mouseY); return true; } } } return super.mouseClicked(mouseX, mouseY, button); } @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { if (activeTextField != null && activeTextField.isFocused()) { return activeTextField.keyPressed(keyCode, scanCode, modifiers); } return super.keyPressed(keyCode, scanCode, modifiers); } @Override public boolean charTyped(char chr, int modifiers) { if (activeTextField != null && activeTextField.isFocused()) { return activeTextField.charTyped(chr, modifiers); } return super.charTyped(chr, modifiers); } @Override public boolean mouseReleased(double mouseX, double mouseY, int button) { scrolling = false; return super.mouseReleased(mouseX, mouseY, button); } @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; int listTop = TOP_PADDING; if (scrolling && contentHeight > listHeight) { float scrollAmount = (float) deltaY / (listHeight - Math.max(20, (int) ((float) listHeight / (float) contentHeight * listHeight))); float maxScroll = contentHeight - listHeight; scrollPosition = MathHelper.clamp(scrollPosition + scrollAmount * maxScroll, 0.0F, maxScroll); return true; } return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); } @Override public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { int listWidth = this.width - (SIDE_PADDING * 2); int listHeight = this.height - TOP_PADDING - BOTTOM_PADDING; int listLeft = SIDE_PADDING; int listTop = TOP_PADDING; if (mouseX >= listLeft && mouseX <= listLeft + listWidth && mouseY >= listTop && mouseY <= listTop + listHeight && contentHeight > listHeight) { float maxScroll = contentHeight - listHeight; scrollPosition = MathHelper.clamp(scrollPosition - (float) verticalAmount * 10, 0.0F, maxScroll); return true; } return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); } private String formatFieldName(String rawName) { StringBuilder result = new StringBuilder(); char[] chars = rawName.toCharArray(); result.append(Character.toUpperCase(chars[0])); for (int i = 1; i < chars.length; i++) { if (Character.isUpperCase(chars[i])) { result.append(' '); } result.append(chars[i]); } return result.toString(); } /** * Base class for all config options */ private abstract class ConfigOption { protected final String name; protected final Field field; protected final int width; protected T value; protected int x; protected int y; public ConfigOption(String name, Field field, T initialValue, int x, int y, int width) { this.name = name; this.field = field; this.value = initialValue; this.x = x; this.y = y; this.width = width; } public abstract void render(DrawContext context, int mouseX, int mouseY, float delta); public abstract void onClick(double mouseX, double mouseY); public boolean isMouseOver(double mouseX, double mouseY) { return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + OPTION_HEIGHT; } protected abstract void saveValue(); } /** * Implementation for boolean config options */ private class BooleanConfigOption extends ConfigOption { private static final int BUTTON_WIDTH = 40; private static final int BUTTON_HEIGHT = 20; public BooleanConfigOption(String name, Field field, Boolean initialValue, int x, int y, int width) { super(name, field, initialValue, x, y, width); } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); int buttonX = x + width - BUTTON_WIDTH - 5; int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2; boolean hovered = isButtonHovered(mouseX, mouseY); int buttonColor = hovered ? 0xFF404040 : 0xFF303030; int buttonBorder = hovered ? 0xFFCCCCCC : 0xFF808080; context.fill(buttonX, buttonY, buttonX + BUTTON_WIDTH, buttonY + BUTTON_HEIGHT, buttonBorder); context.fill(buttonX + 1, buttonY + 1, buttonX + BUTTON_WIDTH - 1, buttonY + BUTTON_HEIGHT - 1, buttonColor); String buttonText = value ? "true" : "false"; int textColor = value ? 0x00be00 : 0xbe0000; int textWidth = textRenderer.getWidth(buttonText); context.drawText( textRenderer, buttonText, buttonX + (BUTTON_WIDTH - textWidth) / 2, buttonY + (BUTTON_HEIGHT - 8) / 2, textColor, true ); } public boolean isButtonHovered(double mouseX, double mouseY) { int buttonX = x + width - BUTTON_WIDTH - 5; int buttonY = y + (OPTION_HEIGHT - BUTTON_HEIGHT) / 2; return mouseX >= buttonX && mouseX <= buttonX + BUTTON_WIDTH && mouseY >= buttonY && mouseY <= buttonY + BUTTON_HEIGHT; } @Override public void onClick(double mouseX, double mouseY) { if (isButtonHovered(mouseX, mouseY)) { value = !value; saveValue(); } } @Override protected void saveValue() { try { // Update config and save field.setBoolean(ClientConfigManager.getConfig(), value); ClientConfigManager.save(); } catch (Exception e) { e.printStackTrace(); } } } /** * Implementation for String config options */ private class StringConfigOption extends ConfigOption { private static final int FIELD_WIDTH = 60; private static final int FIELD_HEIGHT = 16; protected TextFieldWidget textField; public StringConfigOption(String name, Field field, String initialValue, int x, int y, int width) { super(name, field, initialValue, x, y, width); } public void createTextField(net.minecraft.client.MinecraftClient client) { int fieldX = x + width - FIELD_WIDTH - 5; int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; textField = new TextFieldWidget( textRenderer, fieldX, fieldY, FIELD_WIDTH, FIELD_HEIGHT, Text.literal("") ); textField.setText(value); textField.setMaxLength(10); textField.setChangedListener(this::onTextChanged); } public TextFieldWidget getTextField() { return textField; } public void updateTextFieldPosition() { if (textField != null) { int fieldX = x + width - FIELD_WIDTH - 5; int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; textField.setX(fieldX); textField.setY(fieldY); } } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); if (textField != null) { textField.render(context, mouseX, mouseY, delta); } } @Override public void onClick(double mouseX, double mouseY) { } public void onTextChanged(String text) { try { value = text; saveValue(); } catch (NumberFormatException e) {} } @Override protected void saveValue() { try { field.set(ClientConfigManager.getConfig(), value); ClientConfigManager.save(); } catch (Exception e) { e.printStackTrace(); } } } /** * Base class for numeric config options (int and float) */ private abstract class NumericConfigOption extends ConfigOption { private static final int FIELD_WIDTH = 60; private static final int FIELD_HEIGHT = 16; protected TextFieldWidget textField; public NumericConfigOption(String name, Field field, T initialValue, int x, int y, int width) { super(name, field, initialValue, x, y, width); } public void createTextField(net.minecraft.client.MinecraftClient client) { int fieldX = x + width - FIELD_WIDTH - 5; int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; textField = new TextFieldWidget( textRenderer, fieldX, fieldY, FIELD_WIDTH, FIELD_HEIGHT, Text.literal("") ); textField.setText(value.toString()); textField.setMaxLength(10); textField.setChangedListener(this::onTextChanged); } public TextFieldWidget getTextField() { return textField; } public void updateTextFieldPosition() { if (textField != null) { int fieldX = x + width - FIELD_WIDTH - 5; int fieldY = y + (OPTION_HEIGHT - FIELD_HEIGHT) / 2; textField.setX(fieldX); textField.setY(fieldY); } } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { context.drawText(textRenderer, name, x + 5, y + (OPTION_HEIGHT - 8) / 2, 0xFFFFFF, true); if (textField != null) { textField.render(context, mouseX, mouseY, delta); } } @Override public void onClick(double mouseX, double mouseY) {} protected abstract void onTextChanged(String text); } /** * Implementation for integer config options */ private class IntegerConfigOption extends NumericConfigOption { public IntegerConfigOption(String name, Field field, Integer initialValue, int x, int y, int width) { super(name, field, initialValue, x, y, width); } @Override protected void onTextChanged(String text) { try { value = Integer.parseInt(text); saveValue(); } catch (NumberFormatException e) {} } @Override protected void saveValue() { try { field.setInt(ClientConfigManager.getConfig(), value); ClientConfigManager.save(); } catch (Exception e) { e.printStackTrace(); } } } /** * Implementation for float config options */ private class FloatConfigOption extends NumericConfigOption { public FloatConfigOption(String name, Field field, Float initialValue, int x, int y, int width) { super(name, field, initialValue, x, y, width); } @Override protected void onTextChanged(String text) { try { value = Float.parseFloat(text); saveValue(); } catch (NumberFormatException e) {} } @Override protected void saveValue() { try { field.setFloat(ClientConfigManager.getConfig(), value); ClientConfigManager.save(); } catch (Exception e) { e.printStackTrace(); } } } }