From 13c7ba45ff201423eb8dba8a40cfb66ebb531439 Mon Sep 17 00:00:00 2001 From: Xander Date: Tue, 25 Apr 2023 16:28:41 +0100 Subject: Architectury! (#61) --- .github/workflows/gradle.yml | 12 +- .gitignore | 1 + build.gradle.kts | 260 +++------- common/.gitignore | 42 ++ common/build.gradle.kts | 52 ++ .../main/java/dev/isxander/yacl/api/Binding.java | 64 +++ .../java/dev/isxander/yacl/api/ButtonOption.java | 66 +++ .../java/dev/isxander/yacl/api/ConfigCategory.java | 94 ++++ .../java/dev/isxander/yacl/api/Controller.java | 28 + .../java/dev/isxander/yacl/api/LabelOption.java | 41 ++ .../java/dev/isxander/yacl/api/ListOption.java | 152 ++++++ .../dev/isxander/yacl/api/ListOptionEntry.java | 23 + .../java/dev/isxander/yacl/api/NameableEnum.java | 10 + .../main/java/dev/isxander/yacl/api/Option.java | 227 ++++++++ .../java/dev/isxander/yacl/api/OptionAddable.java | 19 + .../java/dev/isxander/yacl/api/OptionFlag.java | 23 + .../java/dev/isxander/yacl/api/OptionGroup.java | 94 ++++ .../dev/isxander/yacl/api/PlaceholderCategory.java | 52 ++ .../dev/isxander/yacl/api/YetAnotherConfigLib.java | 107 ++++ .../dev/isxander/yacl/api/utils/Dimension.java | 33 ++ .../isxander/yacl/api/utils/MutableDimension.java | 11 + .../dev/isxander/yacl/api/utils/OptionUtils.java | 39 ++ .../java/dev/isxander/yacl/config/ConfigEntry.java | 11 + .../dev/isxander/yacl/config/ConfigInstance.java | 48 ++ .../isxander/yacl/config/GsonConfigInstance.java | 212 ++++++++ .../java/dev/isxander/yacl/gui/AbstractWidget.java | 107 ++++ .../dev/isxander/yacl/gui/CategoryListWidget.java | 99 ++++ .../java/dev/isxander/yacl/gui/CategoryWidget.java | 38 ++ .../isxander/yacl/gui/ElementListWidgetExt.java | 177 +++++++ .../isxander/yacl/gui/LowProfileButtonWidget.java | 28 + .../dev/isxander/yacl/gui/OptionListWidget.java | 570 +++++++++++++++++++++ .../isxander/yacl/gui/RequireRestartScreen.java | 21 + .../dev/isxander/yacl/gui/SearchFieldWidget.java | 66 +++ .../isxander/yacl/gui/TextScaledButtonWidget.java | 34 ++ .../dev/isxander/yacl/gui/TooltipButtonWidget.java | 33 ++ .../java/dev/isxander/yacl/gui/YACLScreen.java | 319 ++++++++++++ .../yacl/gui/controllers/ActionController.java | 120 +++++ .../yacl/gui/controllers/BooleanController.java | 157 ++++++ .../yacl/gui/controllers/ColorController.java | 221 ++++++++ .../yacl/gui/controllers/ControllerWidget.java | 170 ++++++ .../yacl/gui/controllers/LabelController.java | 193 +++++++ .../yacl/gui/controllers/ListEntryWidget.java | 135 +++++ .../yacl/gui/controllers/TickBoxController.java | 120 +++++ .../cycling/CyclingControllerElement.java | 60 +++ .../controllers/cycling/CyclingListController.java | 79 +++ .../gui/controllers/cycling/EnumController.java | 60 +++ .../controllers/cycling/ICyclingController.java | 38 ++ .../yacl/gui/controllers/package-info.java | 12 + .../controllers/slider/DoubleSliderController.java | 114 +++++ .../controllers/slider/FloatSliderController.java | 114 +++++ .../gui/controllers/slider/ISliderController.java | 54 ++ .../slider/IntegerSliderController.java | 111 ++++ .../controllers/slider/LongSliderController.java | 111 ++++ .../slider/SliderControllerElement.java | 164 ++++++ .../yacl/gui/controllers/slider/package-info.java | 10 + .../gui/controllers/string/IStringController.java | 44 ++ .../gui/controllers/string/StringController.java | 37 ++ .../string/StringControllerElement.java | 408 +++++++++++++++ .../string/number/DoubleFieldController.java | 104 ++++ .../string/number/FloatFieldController.java | 104 ++++ .../string/number/IntegerFieldController.java | 109 ++++ .../string/number/LongFieldController.java | 109 ++++ .../string/number/NumberFieldController.java | 69 +++ .../controllers/string/number/package-info.java | 10 + .../java/dev/isxander/yacl/gui/utils/GuiUtils.java | 41 ++ .../dev/isxander/yacl/impl/ButtonOptionImpl.java | 218 ++++++++ .../dev/isxander/yacl/impl/ConfigCategoryImpl.java | 137 +++++ .../dev/isxander/yacl/impl/GenericBindingImpl.java | 35 ++ .../dev/isxander/yacl/impl/LabelOptionImpl.java | 154 ++++++ .../isxander/yacl/impl/ListOptionEntryImpl.java | 149 ++++++ .../dev/isxander/yacl/impl/ListOptionImpl.java | 338 ++++++++++++ .../dev/isxander/yacl/impl/OptionGroupImpl.java | 129 +++++ .../java/dev/isxander/yacl/impl/OptionImpl.java | 303 +++++++++++ .../yacl/impl/PlaceholderCategoryImpl.java | 99 ++++ .../yacl/impl/YetAnotherConfigLibImpl.java | 122 +++++ .../yacl/impl/utils/DimensionIntegerImpl.java | 115 +++++ .../isxander/yacl/impl/utils/YACLConstants.java | 8 + .../yacl/mixin/AbstractSelectionListMixin.java | 25 + .../yacl/mixin/OptionInstanceAccessor.java | 13 + common/src/main/resources/architectury.common.json | 3 + .../assets/yet_another_config_lib/lang/el_gr.json | 23 + .../assets/yet_another_config_lib/lang/en_us.json | 29 ++ .../assets/yet_another_config_lib/lang/et_ee.json | 18 + .../assets/yet_another_config_lib/lang/fr_fr.json | 29 ++ .../assets/yet_another_config_lib/lang/pl_pl.json | 23 + .../assets/yet_another_config_lib/lang/pt_br.json | 18 + .../assets/yet_another_config_lib/lang/ru_ru.json | 24 + .../assets/yet_another_config_lib/lang/sl_si.json | 22 + .../assets/yet_another_config_lib/lang/zh_cn.json | 29 ++ .../assets/yet_another_config_lib/lang/zh_tw.json | 23 + common/src/main/resources/yacl-128x.png | Bin 0 -> 13813 bytes common/src/main/resources/yacl.accesswidener | 6 + common/src/main/resources/yacl.mixins.json | 12 + fabric/.gitignore | 42 ++ fabric/build.gradle.kts | 158 ++++++ fabric/src/main/resources/fabric.mod.json | 35 ++ forge/.gitignore | 42 ++ forge/build.gradle.kts | 164 ++++++ forge/gradle.properties | 1 + .../isxander/yacl/forge/YACLForgeEntrypoint.java | 11 + forge/src/main/resources/META-INF/mods.toml | 28 + forge/src/main/resources/pack.mcmeta | 6 + gradle.properties | 12 +- gradle/libs.versions.toml | 48 ++ gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 6 - gradlew.bat | 14 +- settings.gradle.kts | 14 + src/client/java/dev/isxander/yacl/api/Binding.java | 64 --- .../java/dev/isxander/yacl/api/ButtonOption.java | 66 --- .../java/dev/isxander/yacl/api/ConfigCategory.java | 94 ---- .../java/dev/isxander/yacl/api/Controller.java | 28 - .../java/dev/isxander/yacl/api/LabelOption.java | 41 -- .../java/dev/isxander/yacl/api/ListOption.java | 152 ------ .../dev/isxander/yacl/api/ListOptionEntry.java | 23 - .../java/dev/isxander/yacl/api/NameableEnum.java | 10 - src/client/java/dev/isxander/yacl/api/Option.java | 227 -------- .../java/dev/isxander/yacl/api/OptionAddable.java | 19 - .../java/dev/isxander/yacl/api/OptionFlag.java | 23 - .../java/dev/isxander/yacl/api/OptionGroup.java | 94 ---- .../dev/isxander/yacl/api/PlaceholderCategory.java | 52 -- .../dev/isxander/yacl/api/YetAnotherConfigLib.java | 107 ---- .../dev/isxander/yacl/api/utils/OptionUtils.java | 39 -- .../java/dev/isxander/yacl/gui/AbstractWidget.java | 107 ---- .../dev/isxander/yacl/gui/CategoryListWidget.java | 99 ---- .../java/dev/isxander/yacl/gui/CategoryWidget.java | 38 -- .../isxander/yacl/gui/ElementListWidgetExt.java | 179 ------- .../isxander/yacl/gui/LowProfileButtonWidget.java | 29 -- .../dev/isxander/yacl/gui/OptionListWidget.java | 568 -------------------- .../isxander/yacl/gui/RequireRestartScreen.java | 21 - .../dev/isxander/yacl/gui/SearchFieldWidget.java | 66 --- .../isxander/yacl/gui/TextScaledButtonWidget.java | 34 -- .../dev/isxander/yacl/gui/TooltipButtonWidget.java | 34 -- .../java/dev/isxander/yacl/gui/YACLScreen.java | 316 ------------ .../yacl/gui/controllers/ActionController.java | 120 ----- .../yacl/gui/controllers/BooleanController.java | 157 ------ .../yacl/gui/controllers/ColorController.java | 221 -------- .../yacl/gui/controllers/ControllerWidget.java | 170 ------ .../yacl/gui/controllers/LabelController.java | 193 ------- .../yacl/gui/controllers/ListEntryWidget.java | 133 ----- .../yacl/gui/controllers/TickBoxController.java | 120 ----- .../cycling/CyclingControllerElement.java | 60 --- .../controllers/cycling/CyclingListController.java | 79 --- .../gui/controllers/cycling/EnumController.java | 60 --- .../controllers/cycling/ICyclingController.java | 38 -- .../yacl/gui/controllers/package-info.java | 12 - .../controllers/slider/DoubleSliderController.java | 114 ----- .../controllers/slider/FloatSliderController.java | 114 ----- .../gui/controllers/slider/ISliderController.java | 54 -- .../slider/IntegerSliderController.java | 111 ---- .../controllers/slider/LongSliderController.java | 111 ---- .../slider/SliderControllerElement.java | 164 ------ .../yacl/gui/controllers/slider/package-info.java | 10 - .../gui/controllers/string/IStringController.java | 44 -- .../gui/controllers/string/StringController.java | 40 -- .../string/StringControllerElement.java | 408 --------------- .../string/number/DoubleFieldController.java | 104 ---- .../string/number/FloatFieldController.java | 104 ---- .../string/number/IntegerFieldController.java | 109 ---- .../string/number/LongFieldController.java | 109 ---- .../string/number/NumberFieldController.java | 69 --- .../controllers/string/number/package-info.java | 10 - .../java/dev/isxander/yacl/gui/utils/GuiUtils.java | 41 -- .../dev/isxander/yacl/impl/ButtonOptionImpl.java | 218 -------- .../dev/isxander/yacl/impl/ConfigCategoryImpl.java | 137 ----- .../dev/isxander/yacl/impl/GenericBindingImpl.java | 35 -- .../dev/isxander/yacl/impl/LabelOptionImpl.java | 154 ------ .../isxander/yacl/impl/ListOptionEntryImpl.java | 149 ------ .../dev/isxander/yacl/impl/ListOptionImpl.java | 338 ------------ .../dev/isxander/yacl/impl/OptionGroupImpl.java | 129 ----- .../java/dev/isxander/yacl/impl/OptionImpl.java | 304 ----------- .../yacl/impl/PlaceholderCategoryImpl.java | 99 ---- .../yacl/impl/YetAnotherConfigLibImpl.java | 122 ----- .../mixin/client/AbstractSelectionListMixin.java | 26 - .../yacl/mixin/client/OptionInstanceAccessor.java | 13 - .../yet-another-config-lib.client.mixins.json | 14 - .../dev/isxander/yacl/api/utils/Dimension.java | 33 -- .../isxander/yacl/api/utils/MutableDimension.java | 11 - .../java/dev/isxander/yacl/config/ConfigEntry.java | 11 - .../dev/isxander/yacl/config/ConfigInstance.java | 48 -- .../isxander/yacl/config/GsonConfigInstance.java | 212 -------- .../yacl/impl/utils/DimensionIntegerImpl.java | 115 ----- .../isxander/yacl/impl/utils/YACLConstants.java | 8 - .../assets/yet-another-config-lib/lang/el_gr.json | 23 - .../assets/yet-another-config-lib/lang/en_us.json | 29 -- .../assets/yet-another-config-lib/lang/et_ee.json | 18 - .../assets/yet-another-config-lib/lang/fr_fr.json | 29 -- .../assets/yet-another-config-lib/lang/pl_pl.json | 23 - .../assets/yet-another-config-lib/lang/pt_br.json | 18 - .../assets/yet-another-config-lib/lang/ru_ru.json | 24 - .../assets/yet-another-config-lib/lang/sl_si.json | 22 - .../assets/yet-another-config-lib/lang/zh_cn.json | 29 -- .../assets/yet-another-config-lib/lang/zh_tw.json | 23 - src/main/resources/fabric.mod.json | 39 -- src/main/resources/yacl-128x.png | Bin 13813 -> 0 bytes src/main/resources/yacl.accesswidener | 6 - .../java/dev/isxander/yacl/test/ExampleMod.java | 11 - .../isxander/yacl/test/config/ExampleConfig.java | 44 -- .../dev/isxander/yacl/test/config/GuiTest.java | 449 ---------------- .../yacl/test/mixins/TitleScreenMixin.java | 30 -- src/testmod/resources/fabric.mod.json | 22 - .../yet-another-config-lib.test.mixins.json | 11 - test-common/.gitignore | 42 ++ test-common/build.gradle.kts | 24 + .../java/dev/isxander/yacl/test/ConfigTest.java | 51 ++ .../main/java/dev/isxander/yacl/test/GuiTest.java | 462 +++++++++++++++++ test-fabric/.gitignore | 42 ++ test-fabric/build.gradle.kts | 57 +++ .../yacl/test/fabric/ModMenuEntrypoint.java | 12 + test-fabric/src/main/resources/fabric.mod.json | 13 + test-forge/.gitignore | 42 ++ test-forge/build.gradle.kts | 63 +++ test-forge/gradle.properties | 1 + .../dev/isxander/yacl/test/forge/ForgeTest.java | 18 + test-forge/src/main/resources/META-INF/mods.toml | 28 + test-forge/src/main/resources/pack.mcmeta | 6 + 216 files changed, 9628 insertions(+), 8847 deletions(-) create mode 100644 common/.gitignore create mode 100644 common/build.gradle.kts create mode 100644 common/src/main/java/dev/isxander/yacl/api/Binding.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ButtonOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/Controller.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/LabelOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ListOption.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/NameableEnum.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/Option.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionAddable.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionFlag.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/OptionGroup.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java create mode 100644 common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java create mode 100644 common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java create mode 100644 common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/GenericBindingImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java create mode 100644 common/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java create mode 100644 common/src/main/java/dev/isxander/yacl/mixin/AbstractSelectionListMixin.java create mode 100644 common/src/main/java/dev/isxander/yacl/mixin/OptionInstanceAccessor.java create mode 100644 common/src/main/resources/architectury.common.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/el_gr.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/en_us.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/et_ee.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/fr_fr.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/pl_pl.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/pt_br.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/ru_ru.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/sl_si.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/zh_cn.json create mode 100644 common/src/main/resources/assets/yet_another_config_lib/lang/zh_tw.json create mode 100644 common/src/main/resources/yacl-128x.png create mode 100644 common/src/main/resources/yacl.accesswidener create mode 100644 common/src/main/resources/yacl.mixins.json create mode 100644 fabric/.gitignore create mode 100644 fabric/build.gradle.kts create mode 100644 fabric/src/main/resources/fabric.mod.json create mode 100644 forge/.gitignore create mode 100644 forge/build.gradle.kts create mode 100644 forge/gradle.properties create mode 100644 forge/src/main/java/dev/isxander/yacl/forge/YACLForgeEntrypoint.java create mode 100644 forge/src/main/resources/META-INF/mods.toml create mode 100644 forge/src/main/resources/pack.mcmeta create mode 100644 gradle/libs.versions.toml delete mode 100644 src/client/java/dev/isxander/yacl/api/Binding.java delete mode 100644 src/client/java/dev/isxander/yacl/api/ButtonOption.java delete mode 100644 src/client/java/dev/isxander/yacl/api/ConfigCategory.java delete mode 100644 src/client/java/dev/isxander/yacl/api/Controller.java delete mode 100644 src/client/java/dev/isxander/yacl/api/LabelOption.java delete mode 100644 src/client/java/dev/isxander/yacl/api/ListOption.java delete mode 100644 src/client/java/dev/isxander/yacl/api/ListOptionEntry.java delete mode 100644 src/client/java/dev/isxander/yacl/api/NameableEnum.java delete mode 100644 src/client/java/dev/isxander/yacl/api/Option.java delete mode 100644 src/client/java/dev/isxander/yacl/api/OptionAddable.java delete mode 100644 src/client/java/dev/isxander/yacl/api/OptionFlag.java delete mode 100644 src/client/java/dev/isxander/yacl/api/OptionGroup.java delete mode 100644 src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java delete mode 100644 src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java delete mode 100644 src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/AbstractWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/CategoryWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/ElementListWidgetExt.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/OptionListWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/RequireRestartScreen.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/YACLScreen.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ActionController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/BooleanController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ColorController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/TickBoxController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/package-info.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/slider/package-info.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/IStringController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/StringController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java delete mode 100644 src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/GenericBindingImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/OptionImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java delete mode 100644 src/client/java/dev/isxander/yacl/mixin/client/AbstractSelectionListMixin.java delete mode 100644 src/client/java/dev/isxander/yacl/mixin/client/OptionInstanceAccessor.java delete mode 100644 src/client/resources/yet-another-config-lib.client.mixins.json delete mode 100644 src/main/java/dev/isxander/yacl/api/utils/Dimension.java delete mode 100644 src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java delete mode 100644 src/main/java/dev/isxander/yacl/config/ConfigEntry.java delete mode 100644 src/main/java/dev/isxander/yacl/config/ConfigInstance.java delete mode 100644 src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java delete mode 100644 src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java delete mode 100644 src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/el_gr.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/en_us.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/et_ee.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/fr_fr.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/pl_pl.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/pt_br.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/ru_ru.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/sl_si.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/zh_cn.json delete mode 100644 src/main/resources/assets/yet-another-config-lib/lang/zh_tw.json delete mode 100644 src/main/resources/fabric.mod.json delete mode 100644 src/main/resources/yacl-128x.png delete mode 100644 src/main/resources/yacl.accesswidener delete mode 100644 src/testmod/java/dev/isxander/yacl/test/ExampleMod.java delete mode 100644 src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java delete mode 100644 src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java delete mode 100644 src/testmod/java/dev/isxander/yacl/test/mixins/TitleScreenMixin.java delete mode 100644 src/testmod/resources/fabric.mod.json delete mode 100644 src/testmod/resources/yet-another-config-lib.test.mixins.json create mode 100644 test-common/.gitignore create mode 100644 test-common/build.gradle.kts create mode 100644 test-common/src/main/java/dev/isxander/yacl/test/ConfigTest.java create mode 100644 test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java create mode 100644 test-fabric/.gitignore create mode 100644 test-fabric/build.gradle.kts create mode 100644 test-fabric/src/main/java/dev/isxander/yacl/test/fabric/ModMenuEntrypoint.java create mode 100644 test-fabric/src/main/resources/fabric.mod.json create mode 100644 test-forge/.gitignore create mode 100644 test-forge/build.gradle.kts create mode 100644 test-forge/gradle.properties create mode 100644 test-forge/src/main/java/dev/isxander/yacl/test/forge/ForgeTest.java create mode 100644 test-forge/src/main/resources/META-INF/mods.toml create mode 100644 test-forge/src/main/resources/pack.mcmeta diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index b05f565..3786124 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -3,7 +3,7 @@ name: Gradle CI on: push: branches: - - '*' + - '**' paths-ignore: - 'README.md' - 'LICENSE' @@ -12,7 +12,7 @@ on: - 'changelogs/**' pull_request: branches: - - '*' + - '**' paths-ignore: - 'README.md' - 'LICENSE' @@ -47,4 +47,10 @@ jobs: run: ./gradlew build --no-daemon - uses: actions/upload-artifact@v3 with: - path: build/libs/*.jar + path: | + **/build/libs/ + !build/libs/ + !**/*-dev.jar + !**/*-shadow.jar + !**/*-transformProduction*.jar + !**/test*/ diff --git a/.gitignore b/.gitignore index 2f31456..fa11b06 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build /.idea .gradle !.gitkeep +/.architectury-transformer diff --git a/build.gradle.kts b/build.gradle.kts index dde9614..0daff2c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,221 +1,109 @@ plugins { - java + alias(libs.plugins.architectury.plugin) + alias(libs.plugins.architectury.loom) apply false - id("fabric-loom") version "1.1.+" // 1.0.+ but patch must be 16 or higher - id("io.github.juuxel.loom-quiltflower") version "1.8.+" - - id("com.modrinth.minotaur") version "2.6.+" - id("me.hypherionmc.cursegradle") version "2.+" - id("com.github.breadmoirai.github-release") version "2.+" - `maven-publish` - - id("io.github.p03w.machete") version "1.+" - id("org.ajoberstar.grgit") version "5.0.0" + alias(libs.plugins.unified.publishing) apply false + alias(libs.plugins.github.release) + alias(libs.plugins.grgit) } -val ciRun = System.getenv().containsKey("GITHUB_ACTIONS") - -group = "dev.isxander" -version = "2.4.2" - -if (ciRun) - version = "$version+${grgit.branch.current().name.replace('/', '.')}-SNAPSHOT" - -loom { - splitEnvironmentSourceSets() - - mods { - register("yet-another-config-lib") { - sourceSet(sourceSets["main"]) - sourceSet(sourceSets["client"]) - } - } +architectury { + minecraft = libs.versions.minecraft.get() } -val testmod by sourceSets.registering { - compileClasspath += sourceSets.main.get().compileClasspath - runtimeClasspath += sourceSets.main.get().runtimeClasspath - compileClasspath += sourceSets["client"].compileClasspath - runtimeClasspath += sourceSets["client"].runtimeClasspath -} +val changelogText = file("changelogs/${project.version}.md").takeIf { it.exists() }?.readText() ?: "No changelog provided." +val snapshotVer = "${grgit.branch.current().name.replace('/', '.')}-SNAPSHOT" -loom { - accessWidenerPath.set(file("src/main/resources/yacl.accesswidener")) +allprojects { + apply(plugin = "java") + apply(plugin = "maven-publish") + apply(plugin = "architectury-plugin") - runs { - register("testmod") { - client() - ideConfigGenerated(true) - name("Test Mod") - source(testmod.get()) - } + version = "2.5.0+1.19.4" + group = "dev.isxander" - named("client") { ideConfigGenerated(false) } - named("server") { ideConfigGenerated(false) } + if (System.getenv().containsKey("GITHUB_ACTIONS")) { + version = "$version+$snapshotVer" } - createRemapConfigurations(testmod.get()) -} + pluginManager.withPlugin("base") { + val base = the() -repositories { - mavenCentral() - maven("https://maven.terraformersmc.com") - maven("https://maven.quiltmc.org/repository/release") -} - -val minecraftVersion: String by project -val fabricLoaderVersion: String by project -val qmBuild: String by project - -dependencies { - minecraft("com.mojang:minecraft:$minecraftVersion") - mappings(loom.layered { - mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.$qmBuild:intermediary-v2") - officialMojangMappings() - }) - modImplementation("net.fabricmc:fabric-loader:$fabricLoaderVersion") - - "modClientImplementation"(fabricApi.module("fabric-resource-loader-v0", "0.76.0+1.19.4")) - - "testmodImplementation"(sourceSets.main.get().output) - "testmodImplementation"(sourceSets["client"].output) -} - -java { - withSourcesJar() - withJavadocJar() -} - -tasks { - processResources { - val modId: String by project - val modName: String by project - val modDescription: String by project - val githubProject: String by project - - inputs.property("id", modId) - inputs.property("group", project.group) - inputs.property("name", modName) - inputs.property("description", modDescription) - inputs.property("version", project.version) - inputs.property("github", githubProject) - - filesMatching(listOf("fabric.mod.json", "quilt.mod.json")) { - expand( - "id" to modId, - "group" to project.group, - "name" to modName, - "description" to modDescription, - "version" to project.version, - "github" to githubProject, - ) - } + base.archivesName.set("yet-another-config-lib-${project.name}") } - register("releaseMod") { - group = "mod" + ext["changelogText"] = changelogText - dependsOn("modrinth") - dependsOn("modrinthSyncBody") - dependsOn("curseforge") - dependsOn("publishModPublicationToReleasesRepository") - dependsOn("githubRelease") - } -} - -val changelogText = file("changelogs/${project.version}.md").takeIf { it.exists() }?.readText() ?: "No changelog provided." - -val modrinthId: String by project -if (modrinthId.isNotEmpty()) { - modrinth { - token.set(findProperty("MODRINTH_TOKEN")?.toString()) - projectId.set(modrinthId) - versionNumber.set("${project.version}") - versionType.set("release") - uploadFile.set(tasks["remapJar"]) - gameVersions.set(listOf("1.19.4")) - loaders.set(listOf("fabric", "quilt")) - dependencies { - required.project("fabric-api") + repositories { + mavenCentral() + maven("https://maven.terraformersmc.com/releases") + maven("https://maven.isxander.dev/releases") + maven("https://maven.isxander.dev/snapshots") + maven("https://maven.quiltmc.org/repository/release") + maven("https://api.modrinth.com/maven") { + name = "Modrinth" + content { + includeGroup("maven.modrinth") + } } - changelog.set(changelogText) - syncBodyFrom.set(file("README.md").readText()) + maven("https://jitpack.io") } -} -val curseforgeId: String by project -if (hasProperty("CURSEFORGE_TOKEN") && curseforgeId.isNotEmpty()) { - curseforge { - apiKey = findProperty("CURSEFORGE_TOKEN") - project(closureOf { - mainArtifact(tasks["remapJar"], closureOf { - displayName = "${project.version}" - }) - - id = curseforgeId - releaseType = "release" - addGameVersion("1.19.4") - addGameVersion("Fabric") - addGameVersion("Java 17") - - changelog = changelogText - changelogType = "markdown" - - relations(closureOf { - requiredDependency("fabric-api") - }) - }) - - options(closureOf { - forgeGradleIntegration = false - }) + pluginManager.withPlugin("publishing") { + val publishing = the() + + publishing.repositories { + val username = "XANDER_MAVEN_USER".let { System.getenv(it) ?: findProperty(it) }?.toString() + val password = "XANDER_MAVEN_PASS".let { System.getenv(it) ?: findProperty(it) }?.toString() + if (username != null && password != null) { + maven(url = "https://maven.isxander.dev/releases") { + name = "Releases" + credentials { + this.username = username + this.password = password + } + } + maven(url = "https://maven.isxander.dev/snapshots") { + name = "Snapshots" + credentials { + this.username = username + this.password = password + } + } + } else { + println("Xander Maven credentials not satisfied.") + } + } } } githubRelease { token(findProperty("GITHUB_TOKEN")?.toString()) - val githubProject: String by project + val githubProject: String by rootProject val split = githubProject.split("/") owner(split[0]) repo(split[1]) tagName("${project.version}") targetCommitish(grgit.branch.current().name) body(changelogText) - releaseAssets(tasks["remapJar"].outputs.files) + releaseAssets( + { project(":fabric").tasks["remapJar"].outputs.files }, + { project(":fabric").tasks["remapSourcesJar"].outputs.files }, + { project(":forge").tasks["remapJar"].outputs.files }, + { project(":forge").tasks["remapSourcesJar"].outputs.files }, + ) } -publishing { - publications { - create("mod") { - groupId = "dev.isxander" - artifactId = "yet-another-config-lib" +tasks.register("releaseMod") { + group = "mod" - from(components["java"]) - artifact(tasks["remapSourcesJar"]) - } - } + dependsOn("githubRelease") +} - repositories { - val username = "XANDER_MAVEN_USER".let { System.getenv(it) ?: findProperty(it) }?.toString() - val password = "XANDER_MAVEN_PASS".let { System.getenv(it) ?: findProperty(it) }?.toString() - if (username != null && password != null) { - maven(url = "https://maven.isxander.dev/releases") { - name = "Releases" - credentials { - this.username = username - this.password = password - } - } - maven(url = "https://maven.isxander.dev/snapshots") { - name = "Snapshots" - credentials { - this.username = username - this.password = password - } - } - } else { - println("Xander Maven credentials not satisfied.") - } - } +tasks.register("buildAll") { + group = "mod" + + dependsOn(project(":fabric").tasks["build"]) + dependsOn(project(":forge").tasks["build"]) } diff --git a/common/.gitignore b/common/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/common/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..fb79bd1 --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(libs.plugins.architectury.loom) +} + +base { + archivesName.set("yet-another-config-lib") +} + +architectury { + common("fabric", "forge") +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(file("src/main/resources/yacl.accesswidener")) +} + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:${libs.versions.minecraft.get()}+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + modImplementation(libs.fabric.loader) +} + +java { + withSourcesJar() +} + +tasks { + remapJar { + archiveClassifier.set(null as String?) + + from(rootProject.file("LICENSE")) + } +} + +publishing { + publications { + create("common") { + groupId = "dev.isxander.yacl" + artifactId = "yet-another-config-lib-common" + + from(components["java"]) + } + } +} +tasks.findByPath("publishCommonPublicationToReleasesRepository")?.let { + rootProject.tasks["releaseMod"].dependsOn(it) +} diff --git a/common/src/main/java/dev/isxander/yacl/api/Binding.java b/common/src/main/java/dev/isxander/yacl/api/Binding.java new file mode 100644 index 0000000..b4cd2d0 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/Binding.java @@ -0,0 +1,64 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.impl.GenericBindingImpl; +import dev.isxander.yacl.mixin.OptionInstanceAccessor; +import net.minecraft.client.OptionInstance; +import org.apache.commons.lang3.Validate; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Controls modifying the bound option. + * Provides the default value, a setter and a getter. + */ +public interface Binding { + void setValue(T value); + + T getValue(); + + T defaultValue(); + + /** + * Creates a generic binding. + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + */ + static Binding generic(T def, Supplier getter, Consumer setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + return new GenericBindingImpl<>(def, getter, setter); + } + + /** + * Creates a {@link Binding} for Minecraft's {@link OptionInstance} + */ + static Binding minecraft(OptionInstance minecraftOption) { + Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); + + return new GenericBindingImpl<>( + ((OptionInstanceAccessor) (Object) minecraftOption).getInitialValue(), + minecraftOption::get, + minecraftOption::set + ); + } + + /** + * Creates an immutable binding that has no default and cannot be modified. + * + * @param value the value for the binding + */ + static Binding immutable(T value) { + Validate.notNull(value, "`value` must not be null"); + + return new GenericBindingImpl<>( + value, + () -> value, + changed -> {} + ); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java b/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java new file mode 100644 index 0000000..88e1c4b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ButtonOption.java @@ -0,0 +1,66 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.ButtonOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface ButtonOption extends Option> { + /** + * Action to be executed upon button press + */ + BiConsumer action(); + + static dev.isxander.yacl.api.ButtonOption.Builder createBuilder() { + return new ButtonOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + dev.isxander.yacl.api.ButtonOption.Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the option. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Option.Builder#build()}. + */ + dev.isxander.yacl.api.ButtonOption.Builder tooltip(@NotNull Component... tooltips); + + dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull BiConsumer action); + + /** + * Action to be executed upon button press + * + * @see ButtonOption#action() + */ + @Deprecated + dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull Consumer action); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + dev.isxander.yacl.api.ButtonOption.Builder available(boolean available); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + dev.isxander.yacl.api.ButtonOption.Builder controller(@NotNull Function>> control); + + ButtonOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java b/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java new file mode 100644 index 0000000..7764479 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ConfigCategory.java @@ -0,0 +1,94 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.ConfigCategoryImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Separates {@link Option}s or {@link OptionGroup}s into multiple distinct sections. + * Served to a user as a button in the left column, + * upon pressing, the options list is filled with options contained within this category. + */ +public interface ConfigCategory { + /** + * Name of category, displayed as a button on the left column. + */ + @NotNull Component name(); + + /** + * Gets every {@link OptionGroup} in this category. + */ + @NotNull ImmutableList groups(); + + /** + * Tooltip (or description) of the category. + * Rendered on hover. + */ + @NotNull Component tooltip(); + + /** + * Creates a builder to construct a {@link ConfigCategory} + */ + static Builder createBuilder() { + return new ConfigCategoryImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * Adds an option to the root group of the category. + * To add to another group, use {@link Builder#group(OptionGroup)}. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + @Override + Builder option(@NotNull Option option); + + /** + * Adds multiple options to the root group of the category. + * To add to another group, use {@link Builder#groups(Collection)}. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see ConfigCategory#groups() + * @see OptionGroup#isRoot() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * Adds an option group. + * To add an option to the root group, use {@link Builder#option(Option)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + Builder group(@NotNull OptionGroup group); + + /** + * Adds multiple option groups. + * To add multiple options to the root group, use {@link Builder#options(Collection)} + * To construct a group, use {@link OptionGroup#createBuilder()} + */ + Builder groups(@NotNull Collection groups); + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + ConfigCategory build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/Controller.java b/common/src/main/java/dev/isxander/yacl/api/Controller.java new file mode 100644 index 0000000..0b8e2ed --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/Controller.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.network.chat.Component; + +/** + * Provides a widget to control the option. + */ +public interface Controller { + /** + * Gets the dedicated {@link Option} for this controller + */ + Option option(); + + /** + * Gets the formatted value based on {@link Option#pendingValue()} + */ + Component formatValue(); + + /** + * Provides a widget to display + * + * @param screen parent screen + */ + AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/LabelOption.java b/common/src/main/java/dev/isxander/yacl/api/LabelOption.java new file mode 100644 index 0000000..05c7214 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/LabelOption.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.impl.LabelOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * A label option is an easier way of creating a label with a {@link dev.isxander.yacl.gui.controllers.LabelController}. + * This option is immutable and cannot be disabled. Tooltips are supported through + * {@link Component} styling. + */ +public interface LabelOption extends Option { + @NotNull Component label(); + + /** + * Creates a new label option with the given label, skipping a builder for ease. + */ + static LabelOption create(@NotNull Component label) { + return new LabelOptionImpl(label); + } + + static dev.isxander.yacl.api.LabelOption.Builder createBuilder() { + return new LabelOptionImpl.BuilderImpl(); + } + + interface Builder { + /** + * Appends a line to the label + */ + dev.isxander.yacl.api.LabelOption.Builder line(@NotNull Component line); + + /** + * Appends multiple lines to the label + */ + dev.isxander.yacl.api.LabelOption.Builder lines(@NotNull Collection lines); + + LabelOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ListOption.java b/common/src/main/java/dev/isxander/yacl/api/ListOption.java new file mode 100644 index 0000000..afba8ee --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ListOption.java @@ -0,0 +1,152 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.ListOptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A list option that takes form as an option group for UX. + * You add this option through {@link ConfigCategory.Builder#group(OptionGroup)}. Do NOT add as an option. + * Users can add remove and reshuffle a list type. You can use any controller you wish, there are no dedicated + * controllers for list types. List options do not manipulate your list but get and set the list with a + * regular binding for simplicity. + * + * You may apply option flags like a normal option and collapse like a normal group, it is a merge of them both. + * Methods in this interface marked with {@link ApiStatus.Internal} should not be used, and could be subject to + * change at any time + * @param + */ +public interface ListOption extends OptionGroup, Option> { + @Override + @NotNull ImmutableList> options(); + + /** + * Class of the entry type + */ + @NotNull Class elementTypeClass(); + + @ApiStatus.Internal + ListOptionEntry insertNewEntryToTop(); + + @ApiStatus.Internal + void insertEntry(int index, ListOptionEntry entry); + + @ApiStatus.Internal + int indexOf(ListOptionEntry entry); + + @ApiStatus.Internal + void removeEntry(ListOptionEntry entry); + + @ApiStatus.Internal + void addRefreshListener(Runnable changedListener); + + static Builder createBuilder(Class typeClass) { + return new ListOptionImpl.BuilderImpl<>(typeClass); + } + + interface Builder { + /** + * Sets name of the list, for UX purposes, a name should always be given, + * but isn't enforced. + * + * @see ListOption#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the list. It is displayed like a normal + * group when you hover over the name. Entries do not allow a tooltip. + *

+ * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link dev.isxander.yacl.api.ListOption.Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Sets the value that is used when creating new entries + */ + Builder initial(@NotNull T initialValue); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + Builder controller(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding> binding); + + /** + * Sets the binding for the option. + * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + * @see Binding + */ + Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Dictates if the group should be collapsed by default. + * If not set, it will not be collapsed by default. + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + /** + * Adds a listener to the option. Invoked upon changing any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listener(@NotNull BiConsumer>, List> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing of any of the list's entries. + * + * @see Option#addListener(BiConsumer) + */ + ListOption.Builder listeners(@NotNull Collection>, List>> listeners); + + ListOption build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java b/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java new file mode 100644 index 0000000..e0a3424 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/ListOptionEntry.java @@ -0,0 +1,23 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.NotNull; + +public interface ListOptionEntry extends Option { + ListOption parentGroup(); + + @Override + default @NotNull Class typeClass() { + return parentGroup().elementTypeClass(); + } + + @Override + default @NotNull ImmutableSet flags() { + return parentGroup().flags(); + } + + @Override + default boolean available() { + return parentGroup().available(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java b/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java new file mode 100644 index 0000000..4b04057 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/NameableEnum.java @@ -0,0 +1,10 @@ +package dev.isxander.yacl.api; + +import net.minecraft.network.chat.Component; + +/** + * Used for the default value formatter of {@link dev.isxander.yacl.gui.controllers.cycling.EnumController} + */ +public interface NameableEnum { + Component getDisplayName(); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/Option.java b/common/src/main/java/dev/isxander/yacl/api/Option.java new file mode 100644 index 0000000..a6c0311 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/Option.java @@ -0,0 +1,227 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.impl.OptionImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface Option { + /** + * Name of the option + */ + @NotNull Component name(); + + /** + * Tooltip (or description) of the option. + * Rendered on hover. + */ + @NotNull Component tooltip(); + + /** + * Widget provider for a type of option. + * + * @see dev.isxander.yacl.gui.controllers + */ + @NotNull Controller controller(); + + /** + * Binding for the option. + * Controls setting, getting and default value. + * + * @see Binding + */ + @NotNull Binding binding(); + + /** + * If the option can be configured + */ + boolean available(); + + /** + * Sets if the option can be configured after being built + * + * @see Option#available() + */ + void setAvailable(boolean available); + + /** + * Class of the option type. + * Used by some controllers. + */ + @NotNull Class typeClass(); + + /** + * Tasks that needs to be executed upon applying changes. + */ + @NotNull ImmutableSet flags(); + + /** + * Checks if the pending value is not equal to the current set value + */ + boolean changed(); + + /** + * Value in the GUI, ready to set the actual bound value or be undone. + */ + @NotNull T pendingValue(); + + /** + * Sets the pending value + */ + void requestSet(T value); + + /** + * Applies the pending value to the bound value. + * Cannot be undone. + * + * @return if there were changes to apply {@link Option#changed()} + */ + boolean applyValue(); + + /** + * Sets the pending value to the bound value. + */ + void forgetPendingValue(); + + /** + * Sets the pending value to the default bound value. + */ + void requestSetDefault(); + + /** + * Checks if the current pending value is equal to its default value + */ + boolean isPendingValueDefault(); + + default boolean canResetToDefault() { + return true; + } + + /** + * Adds a listener for when the pending value changes + */ + void addListener(BiConsumer, T> changedListener); + + /** + * Creates a builder to construct an {@link Option} + * + * @param type of the option's value + * @param typeClass used to capture the type + */ + static Builder createBuilder(Class typeClass) { + return new OptionImpl.BuilderImpl<>(typeClass); + } + + interface Builder { + /** + * Sets the name to be used by the option. + * + * @see Option#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the option. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. + */ + Builder tooltip(@NotNull Function tooltipGetter); + + /** + * Sets the tooltip to be used by the option. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. + */ + @Deprecated + Builder tooltip(@NotNull Function... tooltipGetter); + + /** + * Sets the tooltip to be used by the option. + * Can be invoked twice to append more lines. + * No need to wrap the text yourself, the gui does this itself. + * + * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Sets the controller for the option. + * This is how you interact and change the options. + * + * @see dev.isxander.yacl.gui.controllers + */ + Builder controller(@NotNull Function, Controller> control); + + /** + * Sets the binding for the option. + * Used for default, getter and setter. + * + * @see Binding + */ + Builder binding(@NotNull Binding binding); + + /** + * Sets the binding for the option. + * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} + * + * @param def default value of the option, used to reset + * @param getter should return the current value of the option + * @param setter should set the option to the supplied value + * @see Binding + */ + Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter); + + /** + * Sets if the option can be configured + * + * @see Option#available() + */ + Builder available(boolean available); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flag(@NotNull OptionFlag... flag); + + /** + * Adds a flag to the option. + * Upon applying changes, all flags are executed. + * {@link Option#flags()} + */ + Builder flags(@NotNull Collection flags); + + /** + * Instantly invokes the binder's setter when modified in the GUI. + * Prevents the user from undoing the change + *

+ * Does not support {@link Option#flags()}! + */ + Builder instant(boolean instant); + + /** + * Adds a listener to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listener(@NotNull BiConsumer, T> listener); + + /** + * Adds multiple listeners to the option. Invoked upon changing the pending value. + * + * @see Option#addListener(BiConsumer) + */ + Builder listeners(@NotNull Collection, T>> listeners); + + Option build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java b/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java new file mode 100644 index 0000000..57be06c --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionAddable.java @@ -0,0 +1,19 @@ +package dev.isxander.yacl.api; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +public interface OptionAddable { + /** + * Adds an option to an abstract builder. + * To construct an option, use {@link Option#createBuilder(Class)} + */ + OptionAddable option(@NotNull Option option); + + /** + * Adds multiple options to an abstract builder. + * To construct an option, use {@link Option#createBuilder(Class)} + */ + OptionAddable options(@NotNull Collection> options); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java b/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java new file mode 100644 index 0000000..51d57e4 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionFlag.java @@ -0,0 +1,23 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.RequireRestartScreen; +import net.minecraft.client.Minecraft; + +import java.util.function.Consumer; + +/** + * Code that is executed upon certain options being applied. + * Each flag is executed only once per save, no matter the amount of options with the flag. + */ +@FunctionalInterface +public interface OptionFlag extends Consumer { + /** Warns the user that a game restart is required for the changes to take effect */ + OptionFlag GAME_RESTART = client -> client.setScreen(new RequireRestartScreen(client.screen)); + + /** Reloads chunks upon applying (F3+A) */ + OptionFlag RELOAD_CHUNKS = client -> client.levelRenderer.allChanged(); + + OptionFlag WORLD_RENDER_UPDATE = client -> client.levelRenderer.needsUpdate(); + + OptionFlag ASSET_RELOAD = Minecraft::delayTextureReload; +} diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java b/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java new file mode 100644 index 0000000..4fe43c7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/OptionGroup.java @@ -0,0 +1,94 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.impl.OptionGroupImpl; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Serves as a separator between multiple chunks of options + * that may be too similar or too few to be placed in a separate {@link ConfigCategory}. + * Or maybe you just want your config to feel less dense. + */ +public interface OptionGroup { + /** + * Name of the option group, displayed as a separator in the option lists. + * Can be empty. + */ + Component name(); + + /** + * Tooltip displayed on hover. + */ + Component tooltip(); + + /** + * List of all options in the group + */ + @NotNull ImmutableList> options(); + + /** + * Dictates if the group should be collapsed by default. + */ + boolean collapsed(); + + /** + * Always false when using the {@link Builder} + * used to not render the separator if true + */ + boolean isRoot(); + + /** + * Creates a builder to construct a {@link OptionGroup} + */ + static Builder createBuilder() { + return new OptionGroupImpl.BuilderImpl(); + } + + interface Builder extends OptionAddable { + /** + * Sets name of the group, can be {@link Component#empty()} to just separate options, like sodium. + * + * @see OptionGroup#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the option group. + * Can be invoked twice to append more lines. + * No need to wrap the Component yourself, the gui does this itself. + * + * @param tooltips Component lines - merged with a new-line on {@link Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Adds an option to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + @Override + Builder option(@NotNull Option option); + + /** + * Adds multiple options to group. + * To construct an option, use {@link Option#createBuilder(Class)} + * + * @see OptionGroup#options() + */ + @Override + Builder options(@NotNull Collection> options); + + /** + * Dictates if the group should be collapsed by default + * + * @see OptionGroup#collapsed() + */ + Builder collapsed(boolean collapsible); + + OptionGroup build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java b/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java new file mode 100644 index 0000000..3641fad --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/PlaceholderCategory.java @@ -0,0 +1,52 @@ +package dev.isxander.yacl.api; + +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.PlaceholderCategoryImpl; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiFunction; + +/** + * A placeholder category that actually just opens another screen, + * instead of displaying options + */ +public interface PlaceholderCategory extends ConfigCategory { + /** + * Function to create a screen to open upon changing to this category + */ + BiFunction screen(); + + static Builder createBuilder() { + return new PlaceholderCategoryImpl.BuilderImpl(); + } + + interface Builder { + /** + * Sets name of the category + * + * @see ConfigCategory#name() + */ + Builder name(@NotNull Component name); + + /** + * Sets the tooltip to be used by the category. + * Can be invoked twice to append more lines. + * No need to wrap the Component yourself, the gui does this itself. + * + * @param tooltips Component lines - merged with a new-line on {@link dev.isxander.yacl.api.PlaceholderCategory.Builder#build()}. + */ + Builder tooltip(@NotNull Component... tooltips); + + /** + * Screen to open upon selecting this category + * + * @see PlaceholderCategory#screen() + */ + Builder screen(@NotNull BiFunction screenFunction); + + PlaceholderCategory build(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java b/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java new file mode 100644 index 0000000..c6da1d1 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/YetAnotherConfigLib.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl.api; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.config.ConfigInstance; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.YetAnotherConfigLibImpl; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.function.Consumer; + +/** + * Main class of the mod. + * Contains all data and used to provide a {@link Screen} + */ +public interface YetAnotherConfigLib { + /** + * Title of the GUI. Only used for Minecraft narration. + */ + Component title(); + + /** + * Gets all config categories. + */ + ImmutableList categories(); + + /** + * Ran when changes are saved. Can be used to save config to a file etc. + */ + Runnable saveFunction(); + + /** + * Ran every time the YACL screen initialises. Can be paired with FAPI to add custom widgets. + */ + Consumer initConsumer(); + + /** + * Generates a Screen to display based on this instance. + * + * @param parent parent screen to open once closed + */ + Screen generateScreen(@Nullable Screen parent); + + /** + * Creates a builder to construct YACL + */ + static Builder createBuilder() { + return new YetAnotherConfigLibImpl.BuilderImpl(); + } + + /** + * Creates an instance using a {@link ConfigInstance} which autofills the save() builder method. + * This also takes an easy functional interface that provides defaults and config to help build YACL bindings. + */ + static YetAnotherConfigLib create(ConfigInstance configInstance, ConfigBackedBuilder builder) { + return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); + } + + interface Builder { + /** + * Sets title of GUI for Minecraft narration + * + * @see YetAnotherConfigLib#title() + */ + Builder title(@NotNull Component title); + + /** + * Adds a new category. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder category(@NotNull ConfigCategory category); + + /** + * Adds multiple categories at once. + * To create a category you need to use {@link ConfigCategory#createBuilder()} + * + * @see YetAnotherConfigLib#categories() + */ + Builder categories(@NotNull Collection categories); + + /** + * Used to define a save function for when user clicks the Save Changes button + * + * @see YetAnotherConfigLib#saveFunction() + */ + Builder save(@NotNull Runnable saveFunction); + + /** + * Defines a consumer that is accepted every time the YACL screen initialises + * + * @see YetAnotherConfigLib#initConsumer() + */ + Builder screenInit(@NotNull Consumer initConsumer); + + YetAnotherConfigLib build(); + } + + @FunctionalInterface + interface ConfigBackedBuilder { + YetAnotherConfigLib.Builder build(T defaults, T config, YetAnotherConfigLib.Builder builder); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java b/common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java new file mode 100644 index 0000000..0de0a58 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/utils/Dimension.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl.api.utils; + +import dev.isxander.yacl.impl.utils.DimensionIntegerImpl; + +public interface Dimension { + T x(); + T y(); + + T width(); + T height(); + + T xLimit(); + T yLimit(); + + T centerX(); + T centerY(); + + boolean isPointInside(T x, T y); + + MutableDimension clone(); + + Dimension withX(T x); + Dimension withY(T y); + Dimension withWidth(T width); + Dimension withHeight(T height); + + Dimension moved(T x, T y); + Dimension expanded(T width, T height); + + static MutableDimension ofInt(int x, int y, int width, int height) { + return new DimensionIntegerImpl(x, y, width, height); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java b/common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java new file mode 100644 index 0000000..eff0186 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl.api.utils; + +public interface MutableDimension extends Dimension { + MutableDimension setX(T x); + MutableDimension setY(T y); + MutableDimension setWidth(T width); + MutableDimension setHeight(T height); + + MutableDimension move(T x, T y); + MutableDimension expand(T width, T height); +} diff --git a/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java b/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java new file mode 100644 index 0000000..22032bd --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/api/utils/OptionUtils.java @@ -0,0 +1,39 @@ +package dev.isxander.yacl.api.utils; + +import dev.isxander.yacl.api.*; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class OptionUtils { + /** + * Consumes all options, ignoring groups and categories. + * When consumer returns true, this function stops iterating. + */ + public static void consumeOptions(YetAnotherConfigLib yacl, Function, Boolean> consumer) { + for (ConfigCategory category : yacl.categories()) { + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption list) { + if (consumer.apply(list)) return; + } else { + for (Option option : group.options()) { + if (consumer.apply(option)) return; + } + } + + } + } + } + + /** + * Consumes all options, ignoring groups and categories. + * + * @see OptionUtils#consumeOptions(YetAnotherConfigLib, Function) + */ + public static void forEachOptions(YetAnotherConfigLib yacl, Consumer> consumer) { + consumeOptions(yacl, (opt) -> { + consumer.accept(opt); + return false; + }); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java b/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java new file mode 100644 index 0000000..7f04c33 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/ConfigEntry.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface ConfigEntry { +} diff --git a/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java b/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java new file mode 100644 index 0000000..c207161 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/ConfigInstance.java @@ -0,0 +1,48 @@ +package dev.isxander.yacl.config; + +import java.lang.reflect.InvocationTargetException; + +/** + * Responsible for handing the actual config data type. + * Holds the instance along with a final default instance + * to reference default values for options and should not be changed. + * + * Abstract methods to save and load the class, implementations are responsible for + * how it saves and load. + * + * @param config data type + */ +public abstract class ConfigInstance { + private final Class configClass; + private final T defaultInstance; + private T instance; + + public ConfigInstance(Class configClass) { + this.configClass = configClass; + + try { + this.defaultInstance = this.instance = configClass.getConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + throw new IllegalStateException(String.format("Could not create default instance of config for %s. Make sure there is a default constructor!", this.configClass.getSimpleName())); + } + } + + public abstract void save(); + public abstract void load(); + + public T getConfig() { + return this.instance; + } + + protected void setConfig(T instance) { + this.instance = instance; + } + + public T getDefaults() { + return this.defaultInstance; + } + + public Class getConfigClass() { + return this.configClass; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java b/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java new file mode 100644 index 0000000..ad7f550 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java @@ -0,0 +1,212 @@ +package dev.isxander.yacl.config; + +import com.google.gson.*; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +import java.awt.*; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.function.UnaryOperator; + +/** + * Uses GSON to serialize and deserialize config data from JSON to a file. + * + * Only fields annotated with {@link ConfigEntry} are included in the JSON. + * {@link Component}, {@link Style} and {@link Color} have default type adapters, so there is no need to provide them in your GSON instance. + * GSON is automatically configured to format fields as {@code lower_camel_case}. + * + * @param config data type + */ +public class GsonConfigInstance extends ConfigInstance { + private final Gson gson; + private final Path path; + + @Deprecated + public GsonConfigInstance(Class configClass, Path path) { + this(configClass, path, new GsonBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, Gson gson) { + this(configClass, path, gson.newBuilder()); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, UnaryOperator builder) { + this(configClass, path, builder.apply(new GsonBuilder())); + } + + @Deprecated + public GsonConfigInstance(Class configClass, Path path, GsonBuilder builder) { + super(configClass); + this.path = path; + this.gson = builder + .setExclusionStrategies(new ConfigExclusionStrategy()) + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) + .serializeNulls() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create(); + } + + private GsonConfigInstance(Class configClass, Path path, Gson gson, boolean fromBuilder) { + super(configClass); + this.path = path; + this.gson = gson; + } + + @Override + public void save() { + try { + YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName()); + Files.writeString(path, gson.toJson(getConfig()), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void load() { + try { + if (Files.notExists(path)) { + save(); + return; + } + + YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName()); + setConfig(gson.fromJson(Files.readString(path), getConfigClass())); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public Path getPath() { + return this.path; + } + + private static class ConfigExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return fieldAttributes.getAnnotation(ConfigEntry.class) == null; + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return false; + } + } + + public static class ColorTypeAdapter implements JsonSerializer, JsonDeserializer { + @Override + public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { + return new Color(jsonElement.getAsInt(), true); + } + + @Override + public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) { + return new JsonPrimitive(color.getRGB()); + } + } + + /** + * Creates a builder for a GSON config instance. + * @param configClass the config class + * @return a new builder + * @param the config type + */ + public static Builder createBuilder(Class configClass) { + return new Builder<>(configClass); + } + + public static class Builder { + private final Class configClass; + private Path path; + private UnaryOperator gsonBuilder = builder -> builder + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .serializeNulls() + .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) + .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) + .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()); + + private Builder(Class configClass) { + this.configClass = configClass; + } + + /** + * Sets the file path to save and load the config from. + */ + public Builder setPath(Path path) { + this.path = path; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gsonBuilder gson builder to use + */ + public Builder overrideGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder; + return this; + } + + /** + * Sets the GSON instance to use. Overrides all YACL defaults such as: + *
    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * Still respects the exclusion strategy to only serialize {@link ConfigEntry} + * but these can be added to with setExclusionStrategies. + * + * @param gson gson instance to be converted to a builder + */ + public Builder overrideGsonBuilder(Gson gson) { + return this.overrideGsonBuilder(gson.newBuilder()); + } + + /** + * Appends extra configuration to a GSON builder. + * This is the intended way to add functionality to the GSON instance. + *

+ * By default, YACL sets the GSON with the following options: + *

    + *
  • lower_camel_case field naming policy
  • + *
  • null serialization
  • + *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • + *
+ * + * @param gsonBuilder the function to apply to the builder + */ + public Builder appendGsonBuilder(UnaryOperator gsonBuilder) { + this.gsonBuilder = builder -> gsonBuilder.apply(this.gsonBuilder.apply(builder)); + return this; + } + + /** + * Builds the config instance. + * @return the built config instance + */ + public GsonConfigInstance build() { + UnaryOperator gsonBuilder = builder -> this.gsonBuilder.apply(builder) + .addSerializationExclusionStrategy(new ConfigExclusionStrategy()) + .addDeserializationExclusionStrategy(new ConfigExclusionStrategy()); + + return new GsonConfigInstance<>(configClass, path, gsonBuilder.apply(new GsonBuilder()).create(), true); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java new file mode 100644 index 0000000..ae3c83b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java @@ -0,0 +1,107 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.utils.Dimension; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; + +import java.awt.*; + +public abstract class AbstractWidget implements GuiEventListener, Renderable, NarratableEntry { + protected final Minecraft client = Minecraft.getInstance(); + protected final Font textRenderer = client.font; + protected final int inactiveColor = 0xFFA0A0A0; + + private Dimension dim; + + public AbstractWidget(Dimension dim) { + this.dim = dim; + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + public boolean canReset() { + return false; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + if (dim == null) return false; + return this.dim.isPointInside((int) mouseX, (int) mouseY); + } + + public void setDimension(Dimension dim) { + this.dim = dim; + } + + public Dimension getDimension() { + return dim; + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.NONE; + } + + public void unfocus() { + + } + + public boolean matchesSearch(String query) { + return true; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + + } + + protected void drawButtonRect(PoseStack matrices, int x1, int y1, int x2, int y2, boolean hovered, boolean enabled) { + if (x1 > x2) { + int xx1 = x1; + x1 = x2; + x2 = xx1; + } + if (y1 > y2) { + int yy1 = y1; + y1 = y2; + y2 = yy1; + } + int width = x2 - x1; + int height = y2 - y1; + + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, net.minecraft.client.gui.components.AbstractWidget.WIDGETS_LOCATION); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + int i = !enabled ? 0 : hovered ? 2 : 1; + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + RenderSystem.enableDepthTest(); + GuiComponent.blit(matrices, x1, y1, 0, 0, 46 + i * 20, width / 2, height, 256, 256); + GuiComponent.blit(matrices, x1 + width / 2, y1, 0, 200 - width / 2f, 46 + i * 20, width / 2, height, 256, 256); + } + + protected int multiplyColor(int hex, float amount) { + Color color = new Color(hex, true); + + return new Color(Math.max((int)(color.getRed() * amount), 0), + Math.max((int)(color.getGreen() * amount), 0), + Math.max((int)(color.getBlue() * amount), 0), + color.getAlpha()).getRGB(); + } + + public void playDownSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java new file mode 100644 index 0000000..41286ff --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java @@ -0,0 +1,99 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.gui.utils.GuiUtils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; + +import java.util.List; + +public class CategoryListWidget extends ElementListWidgetExt { + private final YACLScreen yaclScreen; + + public CategoryListWidget(Minecraft client, YACLScreen yaclScreen, int screenWidth, int screenHeight) { + super(client, 0, 0, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, true); + this.yaclScreen = yaclScreen; + setRenderBackground(false); + setRenderTopAndBottom(false); + + for (ConfigCategory category : yaclScreen.config.categories()) { + addEntry(new CategoryEntry(category)); + } + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + GuiUtils.enableScissor(0, 0, width, height); + super.render(matrices, mouseX, mouseY, delta); + RenderSystem.disableScissor(); + } + + @Override + public int getRowWidth() { + return Math.min(width - width / 10, 396); + } + + @Override + public int getRowLeft() { + return super.getRowLeft() - 2; + } + + @Override + protected int getScrollbarPosition() { + return width - 2; + } + + @Override + protected void renderBackground(PoseStack matrices) { + + } + + public class CategoryEntry extends Entry { + private final CategoryWidget categoryButton; + public final int categoryIndex; + + public CategoryEntry(ConfigCategory category) { + this.categoryIndex = yaclScreen.config.categories().indexOf(category); + categoryButton = new CategoryWidget( + yaclScreen, + category, + categoryIndex, + getRowLeft(), 0, + getRowWidth(), 20 + ); + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + if (mouseY > y1) { + mouseY = -20; + } + + categoryButton.setY(y); + categoryButton.render(matrices, mouseX, mouseY, tickDelta); + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float tickDelta) { + categoryButton.renderHoveredTooltip(matrices); + } + + @Override + public int getItemHeight() { + return 21; + } + + @Override + public List children() { + return ImmutableList.of(categoryButton); + } + + @Override + public List narratables() { + return ImmutableList.of(categoryButton); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java new file mode 100644 index 0000000..60817a2 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java @@ -0,0 +1,38 @@ +package dev.isxander.yacl.gui; + +import dev.isxander.yacl.api.ConfigCategory; +import net.minecraft.client.sounds.SoundManager; + +public class CategoryWidget extends TooltipButtonWidget { + private final int categoryIndex; + + public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) { + super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> { + screen.searchFieldWidget.setValue(""); + screen.changeCategory(categoryIndex); + }); + this.categoryIndex = categoryIndex; + } + + private boolean isCurrentCategory() { + return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex; + } + + @Override + protected int getTextureY() { + int i = 1; + if (!this.active) { + i = 0; + } else if (this.isHoveredOrFocused() || isCurrentCategory()) { + i = 2; + } + + return 46 + i * 20; + } + + @Override + public void playDownSound(SoundManager soundManager) { + if (!isCurrentCategory()) + super.playDownSound(soundManager); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java new file mode 100644 index 0000000..b177236 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java @@ -0,0 +1,177 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.ContainerObjectSelectionList; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.util.Mth; +import org.jetbrains.annotations.Nullable; + +public class ElementListWidgetExt> extends ContainerObjectSelectionList { + protected final int x, y; + + private double smoothScrollAmount = getScrollAmount(); + private boolean returnSmoothAmount = false; + private final boolean doSmoothScrolling; + + public ElementListWidgetExt(Minecraft client, int x, int y, int width, int height, boolean smoothScrolling) { + super(client, width, height, y, y + height, 22); + this.x = this.x0 = x; + this.y = y; + this.x1 = this.x0 + width; + this.doSmoothScrolling = smoothScrolling; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + // default implementation bases scroll step from total height of entries, this is constant + this.setScrollAmount(this.getScrollAmount() - amount * 20); + return true; + } + + @Override + protected void renderBackground(PoseStack matrices) { + // render transparent background if in-game. + setRenderBackground(minecraft.level == null); + if (minecraft.level != null) + fill(matrices, x0, y0, x1, y1, 0x6B000000); + } + + @Override + protected int getScrollbarPosition() { + // default implementation does not respect left/right + return this.x1 - 2; + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount()); + returnSmoothAmount = true; + super.render(matrices, mouseX, mouseY, delta); + returnSmoothAmount = false; + } + + /** + * awful code to only use smooth scroll state when rendering, + * not other code that needs target scroll amount + */ + @Override + public double getScrollAmount() { + if (returnSmoothAmount && doSmoothScrolling) + return smoothScrollAmount; + + return super.getScrollAmount(); + } + + protected void resetSmoothScrolling() { + this.smoothScrollAmount = getScrollAmount(); + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + for (E entry : children()) { + entry.postRender(matrices, mouseX, mouseY, delta); + } + } + + @Nullable + @Override + protected E getEntryAtPosition(double x, double y) { + y += getScrollAmount(); + + if (x < this.x0 || x > this.x1) + return null; + + int currentY = this.y0 - headerHeight + 4; + for (E entry : children()) { + if (y >= currentY && y <= currentY + entry.getItemHeight()) { + return entry; + } + + currentY += entry.getItemHeight(); + } + + return null; + } + + /* + below code is licensed from cloth-config under LGPL3 + modified to inherit vanilla's EntryListWidget and use yarn mappings + + code is responsible for having dynamic item heights + */ + + @Override + protected int getMaxPosition() { + return children().stream().map(E::getItemHeight).reduce(0, Integer::sum) + headerHeight; + } + + @Override + protected void centerScrollOn(E entry) { + double d = (this.height) / -2d; + for (int i = 0; i < this.children().indexOf(entry) && i < this.getItemCount(); i++) + d += children().get(i).getItemHeight(); + this.setScrollAmount(d); + } + + @Override + protected int getRowTop(int index) { + int integer = y0 + 4 - (int) this.getScrollAmount() + headerHeight; + for (int i = 0; i < children().size() && i < index; i++) + integer += children().get(i).getItemHeight(); + return integer; + } + + @Override + protected void renderList(PoseStack matrices, int mouseX, int mouseY, float delta) { + int left = this.getRowLeft(); + int right = this.getRowWidth(); + int count = this.getItemCount(); + + for(int i = 0; i < count; ++i) { + E entry = children().get(i); + int top = this.getRowTop(i); + int bottom = top + entry.getItemHeight(); + int entryHeight = entry.getItemHeight() - 4; + if (bottom >= this.y0 && top <= this.y1) { + this.renderItem(matrices, mouseX, mouseY, delta, i, left, top, right, entryHeight); + } + } + } + + /* END cloth config code */ + + public abstract static class Entry> extends ContainerObjectSelectionList.Entry { + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (GuiEventListener child : this.children()) { + if (child.mouseClicked(mouseX, mouseY, button)) { + if (button == InputConstants.MOUSE_BUTTON_LEFT) + this.setDragging(true); + return true; + } + } + + return false; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (isDragging() && button == InputConstants.MOUSE_BUTTON_LEFT) { + for (GuiEventListener child : this.children()) { + if (child.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) + return true; + } + } + return false; + } + + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + public int getItemHeight() { + return 22; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java new file mode 100644 index 0000000..e8bf59f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java @@ -0,0 +1,28 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; + +public class LowProfileButtonWidget extends Button { + public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + } + + public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress, Tooltip tooltip) { + this(x, y, width, height, message, onPress); + setTooltip(tooltip); + } + + @Override + public void renderWidget(PoseStack matrices, int mouseX, int mouseY, float deltaTicks) { + if (!isHoveredOrFocused() || !active) { + int j = this.active ? 0xFFFFFF : 0xA0A0A0; + this.renderString(matrices, Minecraft.getInstance().font, j); + } else { + super.renderWidget(matrices, mouseX, mouseY, deltaTicks); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java new file mode 100644 index 0000000..a73ce43 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -0,0 +1,570 @@ +package dev.isxander.yacl.gui; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class OptionListWidget extends ElementListWidgetExt { + private final YACLScreen yaclScreen; + private boolean singleCategory = false; + + private ImmutableList viewableChildren; + + public OptionListWidget(YACLScreen screen, Minecraft client, int width, int height) { + super(client, width / 3, 0, width / 3 * 2 + 1, height, true); + this.yaclScreen = screen; + + refreshOptions(); + + for (ConfigCategory category : screen.config.categories()) { + for (OptionGroup group : category.groups()) { + if (group instanceof ListOption listOption) { + listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); + } + } + } + } + + public void refreshOptions() { + clearEntries(); + + List categories = new ArrayList<>(); + if (yaclScreen.getCurrentCategoryIdx() == -1) { + // -1 = no category, search in progress, so use all categories for search + categories.addAll(yaclScreen.config.categories()); + } else { + categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx())); + } + singleCategory = categories.size() == 1; + + for (ConfigCategory category : categories) { + for (OptionGroup group : category.groups()) { + GroupSeparatorEntry groupSeparatorEntry; + if (!group.isRoot()) { + groupSeparatorEntry = group instanceof ListOption listOption + ? new ListGroupSeparatorEntry(listOption, yaclScreen) + : new GroupSeparatorEntry(group, yaclScreen); + addEntry(groupSeparatorEntry); + } else { + groupSeparatorEntry = null; + } + + List optionEntries = new ArrayList<>(); + + // add empty entry to make sure users know it's empty not just bugging out + if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { + if (listGroupSeparatorEntry.listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); + addEntry(emptyListLabel); + optionEntries.add(emptyListLabel); + } + } + + for (Option option : group.options()) { + OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); + addEntry(entry); + optionEntries.add(entry); + } + + if (groupSeparatorEntry != null) { + groupSeparatorEntry.setChildEntries(optionEntries); + } + } + } + + recacheViewableChildren(); + setScrollAmount(0); + resetSmoothScrolling(); + } + + private void refreshListEntries(ListOption listOption, ConfigCategory category) { + // find group separator for group + ListGroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof ListGroupSeparatorEntry gs && gs.group == listOption).map(ListGroupSeparatorEntry.class::cast).findAny().orElse(null); + + if (groupSeparator == null) { + YACLConstants.LOGGER.warn("Can't find group seperator to refresh list option entries for list option " + listOption.name()); + return; + } + + for (Entry entry : groupSeparator.childEntries) + super.removeEntry(entry); + groupSeparator.childEntries.clear(); + + // if no entries, below loop won't run where addEntryBelow() recaches viewable children + if (listOption.options().isEmpty()) { + EmptyListLabel emptyListLabel; + addEntryBelow(groupSeparator, emptyListLabel = new EmptyListLabel(groupSeparator, category)); + groupSeparator.childEntries.add(emptyListLabel); + return; + } + + Entry lastEntry = groupSeparator; + for (ListOptionEntry listOptionEntry : listOption.options()) { + OptionEntry optionEntry = new OptionEntry(listOptionEntry, category, listOption, groupSeparator, listOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); + addEntryBelow(lastEntry, optionEntry); + groupSeparator.childEntries.add(optionEntry); + lastEntry = optionEntry; + } + } + + public Dimension getDefaultEntryDimension() { + return Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20); + } + + public void expandAllGroups() { + for (Entry entry : super.children()) { + if (entry instanceof GroupSeparatorEntry groupSeparatorEntry) { + groupSeparatorEntry.setExpanded(true); + } + } + } + + @Override + public int getRowWidth() { + return Math.min(396, (int)(width / 1.3f)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Entry child : children()) { + if (child != getEntryAtPosition(mouseX, mouseY) && child instanceof OptionEntry optionEntry) + optionEntry.widget.unfocus(); + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + super.mouseScrolled(mouseX, mouseY, amount); + + for (Entry child : children()) { + if (child.mouseScrolled(mouseX, mouseY, amount)) + break; + } + + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + for (Entry child : children()) { + if (child.keyPressed(keyCode, scanCode, modifiers)) + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + for (Entry child : children()) { + if (child.charTyped(chr, modifiers)) + return true; + } + + return super.charTyped(chr, modifiers); + } + + @Override + protected int getScrollbarPosition() { + return x1 - (int)(width * 0.05f); + } + + public void recacheViewableChildren() { + this.viewableChildren = ImmutableList.copyOf(super.children().stream().filter(Entry::isViewable).toList()); + + // update y positions before they need to be rendered are rendered + int i = 0; + for (Entry entry : viewableChildren) { + if (entry instanceof OptionEntry optionEntry) + optionEntry.widget.setDimension(optionEntry.widget.getDimension().withY(getRowTop(i))); + i++; + } + } + + @Override + public List children() { + return viewableChildren; + } + + public void addEntry(int index, Entry entry) { + super.children().add(index, entry); + recacheViewableChildren(); + } + + public void addEntryBelow(Entry below, Entry entry) { + int idx = super.children().indexOf(below) + 1; + + if (idx == 0) + throw new IllegalStateException("The entry to insert below does not exist!"); + + addEntry(idx, entry); + } + + public void addEntryBelowWithoutScroll(Entry below, Entry entry) { + double d = (double)this.getMaxScroll() - this.getScrollAmount(); + addEntryBelow(below, entry); + setScrollAmount(getMaxScroll() - d); + } + + @Override + public boolean removeEntryFromTop(Entry entry) { + boolean ret = super.removeEntryFromTop(entry); + recacheViewableChildren(); + return ret; + } + + @Override + public boolean removeEntry(Entry entry) { + boolean ret = super.removeEntry(entry); + recacheViewableChildren(); + return ret; + } + + public abstract class Entry extends ElementListWidgetExt.Entry { + public boolean isViewable() { + return true; + } + + protected boolean isHovered() { + return Objects.equals(getHovered(), this); + } + } + + public class OptionEntry extends Entry { + public final Option option; + public final ConfigCategory category; + public final OptionGroup group; + + public final @Nullable GroupSeparatorEntry groupSeparatorEntry; + + public final AbstractWidget widget; + + private final TextScaledButtonWidget resetButton; + + private final String categoryName; + private final String groupName; + + public OptionEntry(Option option, ConfigCategory category, OptionGroup group, @Nullable GroupSeparatorEntry groupSeparatorEntry, AbstractWidget widget) { + this.option = option; + this.category = category; + this.group = group; + this.groupSeparatorEntry = groupSeparatorEntry; + this.widget = widget; + this.categoryName = category.name().getString().toLowerCase(); + this.groupName = group.name().getString().toLowerCase(); + if (option.canResetToDefault() && this.widget.canReset()) { + this.widget.setDimension(this.widget.getDimension().expanded(-20, 0)); + this.resetButton = new TextScaledButtonWidget(widget.getDimension().xLimit(), -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { + option.requestSetDefault(); + }); + option.addListener((opt, val) -> this.resetButton.active = !opt.isPendingValueDefault() && opt.available()); + this.resetButton.active = !option.isPendingValueDefault() && option.available(); + } else { + this.resetButton = null; + } + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + widget.setDimension(widget.getDimension().withY(y)); + + widget.render(matrices, mouseX, mouseY, tickDelta); + + if (resetButton != null) { + resetButton.setY(y); + resetButton.render(matrices, mouseX, mouseY, tickDelta); + } + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + widget.postRender(matrices, mouseX, mouseY, delta); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + return widget.mouseScrolled(mouseX, mouseY, amount); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return widget.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + return widget.charTyped(chr, modifiers); + } + + @Override + public boolean isViewable() { + String query = yaclScreen.searchFieldWidget.getQuery(); + return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) + && (yaclScreen.searchFieldWidget.isEmpty() + || (!singleCategory && categoryName.contains(query)) + || groupName.contains(query) + || widget.matchesSearch(query)); + } + + @Override + public int getItemHeight() { + return Math.max(widget.getDimension().height(), resetButton != null ? resetButton.getHeight() : 0) + 2; + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + } + + @Override + public List narratables() { + if (resetButton == null) + return ImmutableList.of(widget); + + return ImmutableList.of(widget, resetButton); + } + + @Override + public List children() { + if (resetButton == null) + return ImmutableList.of(widget); + + return ImmutableList.of(widget, resetButton); + } + } + + public class GroupSeparatorEntry extends Entry { + protected final OptionGroup group; + protected final MultiLineLabel wrappedName; + protected final MultiLineLabel wrappedTooltip; + + protected final LowProfileButtonWidget expandMinimizeButton; + + protected final Screen screen; + protected final Font font = Minecraft.getInstance().font; + + protected boolean groupExpanded; + + protected List childEntries = new ArrayList<>(); + + private int y; + + private GroupSeparatorEntry(OptionGroup group, Screen screen) { + this.group = group; + this.screen = screen; + this.wrappedName = MultiLineLabel.create(font, group.name(), getRowWidth() - 45); + this.wrappedTooltip = MultiLineLabel.create(font, group.tooltip(), screen.width / 3 * 2 - 10); + this.groupExpanded = !group.collapsed(); + this.expandMinimizeButton = new LowProfileButtonWidget(0, 0, 20, 20, Component.empty(), btn -> onExpandButtonPress()); + updateExpandMinimizeText(); + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + this.y = y; + + int buttonY = y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2 + 1; + + expandMinimizeButton.setY(buttonY); + expandMinimizeButton.setX(x); + expandMinimizeButton.render(matrices, mouseX, mouseY, tickDelta); + + wrappedName.renderCentered(matrices, x + entryWidth / 2, y + getYPadding()); + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + if ((isHovered() && !expandMinimizeButton.isMouseOver(mouseX, mouseY)) || expandMinimizeButton.isFocused()) { + YACLScreen.renderMultilineTooltip(matrices, font, wrappedTooltip, getRowLeft() + getRowWidth() / 2, y - 3, y + getItemHeight() + 3, screen.width, screen.height); + } + } + + public boolean isExpanded() { + return groupExpanded; + } + + public void setExpanded(boolean expanded) { + if (this.groupExpanded == expanded) + return; + + this.groupExpanded = expanded; + updateExpandMinimizeText(); + recacheViewableChildren(); + } + + protected void onExpandButtonPress() { + setExpanded(!isExpanded()); + } + + protected void updateExpandMinimizeText() { + expandMinimizeButton.setMessage(Component.literal(isExpanded() ? "▼" : "▶")); + } + + public void setChildEntries(List childEntries) { + this.childEntries.clear(); + this.childEntries.addAll(childEntries); + } + + @Override + public boolean isViewable() { + return yaclScreen.searchFieldWidget.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); + } + + @Override + public int getItemHeight() { + return Math.max(wrappedName.getLineCount(), 1) * font.lineHeight + getYPadding() * 2; + } + + private int getYPadding() { + return 6; + } + + @Override + public List narratables() { + return ImmutableList.of(new NarratableEntry() { + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.HOVERED; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, group.name()); + builder.add(NarratedElementType.HINT, group.tooltip()); + } + }); + } + + @Override + public List children() { + return ImmutableList.of(expandMinimizeButton); + } + } + + public class ListGroupSeparatorEntry extends GroupSeparatorEntry { + private final ListOption listOption; + private final TextScaledButtonWidget resetListButton; + private final TooltipButtonWidget addListButton; + + private ListGroupSeparatorEntry(ListOption group, Screen screen) { + super(group, screen); + this.listOption = group; + + this.resetListButton = new TextScaledButtonWidget(getRowRight() - 20, -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { + group.requestSetDefault(); + }); + group.addListener((opt, val) -> this.resetListButton.active = !opt.isPendingValueDefault() && opt.available()); + this.resetListButton.active = !group.isPendingValueDefault() && group.available(); + + this.addListButton = new TooltipButtonWidget(yaclScreen, resetListButton.getX() - 20, -50, 20, 20, Component.literal("+"), Component.translatable("yacl.list.add_top"), btn -> { + group.insertNewEntryToTop(); + setExpanded(true); + }); + + updateExpandMinimizeText(); + minimizeIfUnavailable(); + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + updateExpandMinimizeText(); // update every render because option could become available/unavailable at any time + + super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); + + int buttonY = expandMinimizeButton.getY(); + + resetListButton.setY(buttonY); + addListButton.setY(buttonY); + + resetListButton.render(matrices, mouseX, mouseY, tickDelta); + addListButton.render(matrices, mouseX, mouseY, tickDelta); + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + minimizeIfUnavailable(); // cannot run in render because it *should* cause a ConcurrentModificationException (but doesn't) + + super.postRender(matrices, mouseX, mouseY, delta); + + addListButton.renderHoveredTooltip(matrices); + } + + private void minimizeIfUnavailable() { + if (!listOption.available() && isExpanded()) { + setExpanded(false); + } + } + + @Override + protected void updateExpandMinimizeText() { + super.updateExpandMinimizeText(); + expandMinimizeButton.active = listOption == null || listOption.available(); + if (addListButton != null) + addListButton.active = expandMinimizeButton.active; + } + + @Override + public List children() { + return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton); + } + } + + public class EmptyListLabel extends Entry { + private final ListGroupSeparatorEntry parent; + private final String groupName; + private final String categoryName; + + public EmptyListLabel(ListGroupSeparatorEntry parent, ConfigCategory category) { + this.parent = parent; + this.groupName = parent.group.name().getString().toLowerCase(); + this.categoryName = category.name().getString().toLowerCase(); + } + + @Override + public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + drawCenteredString(matrices, Minecraft.getInstance().font, Component.translatable("yacl.list.empty").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), x + entryWidth / 2, y, -1); + } + + @Override + public boolean isViewable() { + String query = yaclScreen.searchFieldWidget.getQuery(); + return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty() + || (!singleCategory && categoryName.contains(query)) + || groupName.contains(query)); + } + + @Override + public int getItemHeight() { + return 11; + } + + @Override + public List children() { + return ImmutableList.of(); + } + + @Override + public List narratables() { + return ImmutableList.of(); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java b/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java new file mode 100644 index 0000000..18b6033 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/RequireRestartScreen.java @@ -0,0 +1,21 @@ +package dev.isxander.yacl.gui; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.ConfirmScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class RequireRestartScreen extends ConfirmScreen { + public RequireRestartScreen(Screen parent) { + super(option -> { + if (option) Minecraft.getInstance().stop(); + else Minecraft.getInstance().setScreen(parent); + }, + Component.translatable("yacl.restart.title").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + Component.translatable("yacl.restart.message"), + Component.translatable("yacl.restart.yes"), + Component.translatable("yacl.restart.no") + ); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java new file mode 100644 index 0000000..5cf38e0 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java @@ -0,0 +1,66 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.network.chat.Component; + +public class SearchFieldWidget extends EditBox { + private Component emptyText; + private final YACLScreen yaclScreen; + private final Font font; + + private boolean isEmpty = true; + + public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText) { + super(font, x, y, width, height, text); + setResponder(string -> update()); + setFilter(string -> !string.endsWith(" ") && !string.startsWith(" ")); + this.yaclScreen = yaclScreen; + this.font = font; + this.emptyText = emptyText; + } + + @Override + public void renderWidget(PoseStack matrices, int mouseX, int mouseY, float delta) { + super.renderWidget(matrices, mouseX, mouseY, delta); + if (isVisible() && isEmpty()) { + font.drawShadow(matrices, emptyText, getX() + 4, this.getY() + (this.height - 8) / 2f, 0x707070); + } + } + + private void update() { + boolean wasEmpty = isEmpty; + isEmpty = getValue().isEmpty(); + + if (isEmpty && wasEmpty) + return; + + if (!isEmpty && yaclScreen.getCurrentCategoryIdx() != -1) + yaclScreen.changeCategory(-1); + if (isEmpty && yaclScreen.getCurrentCategoryIdx() == -1) + yaclScreen.changeCategory(0); + + yaclScreen.optionList.expandAllGroups(); + yaclScreen.optionList.recacheViewableChildren(); + + yaclScreen.optionList.setScrollAmount(0); + yaclScreen.categoryList.setScrollAmount(0); + } + + public String getQuery() { + return getValue().toLowerCase(); + } + + public boolean isEmpty() { + return isEmpty; + } + + public Component getEmptyText() { + return emptyText; + } + + public void setEmptyText(Component emptyText) { + this.emptyText = emptyText; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java new file mode 100644 index 0000000..b955912 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java @@ -0,0 +1,34 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +public class TextScaledButtonWidget extends Button { + public float textScale; + + public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + this.textScale = textScale; + } + + public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress, Tooltip tooltip) { + this(x, y, width, height, textScale, message, onPress); + setTooltip(tooltip); + } + + @Override + public void renderString(PoseStack matrices, Font textRenderer, int color) { + Font font = Minecraft.getInstance().font; + + matrices.pushPose(); + matrices.translate(((this.getX() + this.width / 2f) - font.width(getMessage()) * textScale / 2), (float)this.getY() + (this.height - 8 * textScale) / 2f / textScale, 0); + matrices.scale(textScale, textScale, 1); + font.drawShadow(matrices, getMessage(), 0, 0, color | Mth.ceil(this.alpha * 255.0F) << 24); + matrices.popPose(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java new file mode 100644 index 0000000..3b5b6fc --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/TooltipButtonWidget.java @@ -0,0 +1,33 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class TooltipButtonWidget extends TextScaledButtonWidget { + + protected final Screen screen; + protected MultiLineLabel wrappedDescription; + + public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, float textScale, Component tooltip, OnPress onPress) { + super(x, y, width, height, textScale, message, onPress); + this.screen = screen; + setTooltip(tooltip); + } + + public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, Component tooltip, OnPress onPress) { + this(screen, x, y, width, height, message, 1, tooltip, onPress); + } + + public void renderHoveredTooltip(PoseStack matrices) { + if (isHoveredOrFocused()) { + YACLScreen.renderMultilineTooltip(matrices, Minecraft.getInstance().font, wrappedDescription, getX() + width / 2, getY() - 4, getY() + height + 4, screen.width, screen.height); + } + } + + public void setTooltip(Component tooltip) { + wrappedDescription = MultiLineLabel.create(Minecraft.getInstance().font, tooltip, screen.width / 3 - 5); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java new file mode 100644 index 0000000..3600e61 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java @@ -0,0 +1,319 @@ +package dev.isxander.yacl.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.*; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionFlag; +import dev.isxander.yacl.api.PlaceholderCategory; +import dev.isxander.yacl.api.YetAnotherConfigLib; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.api.utils.MutableDimension; +import dev.isxander.yacl.api.utils.OptionUtils; +import dev.isxander.yacl.gui.utils.GuiUtils; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import org.joml.Matrix4f; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +public class YACLScreen extends Screen { + public final YetAnotherConfigLib config; + private int currentCategoryIdx; + + private final Screen parent; + + public OptionListWidget optionList; + public CategoryListWidget categoryList; + public TooltipButtonWidget finishedSaveButton, cancelResetButton, undoButton; + public SearchFieldWidget searchFieldWidget; + + public Component saveButtonMessage, saveButtonTooltipMessage; + private int saveButtonMessageTime; + + + public YACLScreen(YetAnotherConfigLib config, Screen parent) { + super(config.title()); + this.config = config; + this.parent = parent; + this.currentCategoryIdx = 0; + } + + @Override + protected void init() { + int columnWidth = width / 3; + int padding = columnWidth / 20; + columnWidth = Math.min(columnWidth, 400); + int paddedWidth = columnWidth - padding * 2; + + MutableDimension actionDim = Dimension.ofInt(width / 3 / 2, height - padding - 20, paddedWidth, 20); + finishedSaveButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.empty(), + Component.empty(), + btn -> finishOrSave() + ); + actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22); + cancelResetButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.empty(), + Component.empty(), + btn -> cancelOrReset() + ); + actionDim.move(actionDim.width() + 4, 0); + undoButton = new TooltipButtonWidget( + this, + actionDim.x() - actionDim.width() / 2, + actionDim.y(), + actionDim.width(), + actionDim.height(), + Component.translatable("yacl.gui.undo"), + Component.translatable("yacl.gui.undo.tooltip"), + btn -> undo() + ); + + searchFieldWidget = new SearchFieldWidget( + this, + font, + width / 3 / 2 - paddedWidth / 2 + 1, + undoButton.getY() - 22, + paddedWidth - 2, 18, + Component.translatable("gui.recipebook.search_hint"), + Component.translatable("gui.recipebook.search_hint") + ); + + categoryList = new CategoryListWidget(minecraft, this, width, height); + addWidget(categoryList); + + updateActionAvailability(); + addRenderableWidget(searchFieldWidget); + addRenderableWidget(cancelResetButton); + addRenderableWidget(undoButton); + addRenderableWidget(finishedSaveButton); + + optionList = new OptionListWidget(this, minecraft, width, height); + addWidget(optionList); + + config.initConsumer().accept(this); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + renderBackground(matrices); + + super.render(matrices, mouseX, mouseY, delta); + categoryList.render(matrices, mouseX, mouseY, delta); + searchFieldWidget.render(matrices, mouseX, mouseY, delta); + optionList.render(matrices, mouseX, mouseY, delta); + + categoryList.postRender(matrices, mouseX, mouseY, delta); + optionList.postRender(matrices, mouseX, mouseY, delta); + + for (GuiEventListener child : children()) { + if (child instanceof TooltipButtonWidget tooltipButtonWidget) { + tooltipButtonWidget.renderHoveredTooltip(matrices); + } + } + } + + protected void finishOrSave() { + saveButtonMessage = null; + + if (pendingChanges()) { + Set flags = new HashSet<>(); + OptionUtils.forEachOptions(config, option -> { + if (option.applyValue()) { + flags.addAll(option.flags()); + } + }); + OptionUtils.forEachOptions(config, option -> { + if (option.changed()) { + // if still changed after applying, reset to the current value from binding + // as something has gone wrong. + option.forgetPendingValue(); + YACLConstants.LOGGER.error("Option '{}' value mismatch after applying! Reset to binding's getter.", option.name().getString()); + } + }); + config.saveFunction().run(); + + flags.forEach(flag -> flag.accept(minecraft)); + } else onClose(); + } + + protected void cancelOrReset() { + if (pendingChanges()) { // if pending changes, button acts as a cancel button + OptionUtils.forEachOptions(config, Option::forgetPendingValue); + onClose(); + } else { // if not, button acts as a reset button + OptionUtils.forEachOptions(config, Option::requestSetDefault); + } + } + + protected void undo() { + OptionUtils.forEachOptions(config, Option::forgetPendingValue); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (optionList.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (optionList.charTyped(chr, modifiers)) { + return true; + } + + return super.charTyped(chr, modifiers); + } + + public void changeCategory(int idx) { + if (idx == currentCategoryIdx) + return; + + if (idx != -1 && config.categories().get(idx) instanceof PlaceholderCategory placeholderCategory) { + minecraft.setScreen(placeholderCategory.screen().apply(minecraft, this)); + } else { + currentCategoryIdx = idx; + optionList.refreshOptions(); + } + } + + public int getCurrentCategoryIdx() { + return currentCategoryIdx; + } + + private void updateActionAvailability() { + boolean pendingChanges = pendingChanges(); + + undoButton.active = pendingChanges; + finishedSaveButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE)); + finishedSaveButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip")); + cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset")); + cancelResetButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip")); + } + + @Override + public void tick() { + searchFieldWidget.tick(); + + updateActionAvailability(); + + if (saveButtonMessage != null) { + if (saveButtonMessageTime > 140) { + saveButtonMessage = null; + saveButtonTooltipMessage = null; + saveButtonMessageTime = 0; + } else { + saveButtonMessageTime++; + finishedSaveButton.setMessage(saveButtonMessage); + if (saveButtonTooltipMessage != null) { + finishedSaveButton.setTooltip(saveButtonTooltipMessage); + } + } + } + } + + private void setSaveButtonMessage(Component message, Component tooltip) { + saveButtonMessage = message; + saveButtonTooltipMessage = tooltip; + saveButtonMessageTime = 0; + } + + private boolean pendingChanges() { + AtomicBoolean pendingChanges = new AtomicBoolean(false); + OptionUtils.consumeOptions(config, (option) -> { + if (option.changed()) { + pendingChanges.set(true); + return true; + } + return false; + }); + + return pendingChanges.get(); + } + + @Override + public boolean shouldCloseOnEsc() { + if (pendingChanges()) { + setSaveButtonMessage(Component.translatable("yacl.gui.save_before_exit").withStyle(ChatFormatting.RED), Component.translatable("yacl.gui.save_before_exit.tooltip")); + return false; + } + return true; + } + + @Override + public void onClose() { + minecraft.setScreen(parent); + } + + public static void renderMultilineTooltip(PoseStack matrices, Font font, MultiLineLabel text, int centerX, int yAbove, int yBelow, int screenWidth, int screenHeight) { + if (text.getLineCount() > 0) { + int maxWidth = text.getWidth(); + int lineHeight = font.lineHeight + 1; + int height = text.getLineCount() * lineHeight - 1; + + int belowY = yBelow + 12; + int aboveY = yAbove - height + 12; + int maxBelow = screenHeight - (belowY + height); + int minAbove = aboveY - height; + int y = aboveY; + if (minAbove < 8) + y = maxBelow > minAbove ? belowY : aboveY; + + int x = Math.max(centerX - text.getWidth() / 2 - 12, -6); + + int drawX = x + 12; + int drawY = y - 12; + + matrices.pushPose(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder bufferBuilder = tesselator.getBuilder(); + RenderSystem.setShader(GameRenderer::getPositionColorShader); + bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + Matrix4f matrix4f = matrices.last().pose(); + TooltipRenderUtil.renderTooltipBackground( + GuiComponent::fillGradient, + matrix4f, + bufferBuilder, + drawX, + drawY, + maxWidth, + height, + 400 + ); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + BufferUploader.drawWithShader(bufferBuilder.end()); + RenderSystem.disableBlend(); + matrices.translate(0.0, 0.0, 400.0); + + text.renderLeftAligned(matrices, drawX, drawY, lineHeight, -1); + + matrices.popPose(); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java new file mode 100644 index 0000000..e57cdd2 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java @@ -0,0 +1,120 @@ +package dev.isxander.yacl.gui.controllers; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.yacl.api.ButtonOption; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.network.chat.Component; + +import java.util.function.BiConsumer; + +/** + * Simple controller that simply runs the button action on press + * and renders a {@link} Text on the right. + */ +public class ActionController implements Controller> { + public static final Component DEFAULT_TEXT = Component.translatable("yacl.control.action.execute"); + + private final ButtonOption option; + private final Component text; + + /** + * Constructs an action controller + * with the default formatter of {@link ActionController#DEFAULT_TEXT} + * + * @param option bound option + */ + public ActionController(ButtonOption option) { + this(option, DEFAULT_TEXT); + } + + /** + * Constructs an action controller + * + * @param option bound option + * @param text text to display + */ + public ActionController(ButtonOption option, Component text) { + this.option = option; + this.text = text; + + } + + /** + * {@inheritDoc} + */ + @Override + public ButtonOption option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return text; + } + + /** + * {@inheritDoc} + */ + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new ActionControllerElement(this, screen, widgetDimension); + } + + public static class ActionControllerElement extends ControllerWidget { + private final String buttonString; + + public ActionControllerElement(ActionController control, YACLScreen screen, Dimension dim) { + super(control, screen, dim); + buttonString = control.formatValue().getString().toLowerCase(); + } + + public void executeAction() { + playDownSound(); + control.option().action().accept(screen, control.option()); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isMouseOver(mouseX, mouseY) && isAvailable()) { + executeAction(); + return true; + } + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!focused) { + return false; + } + + if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { + executeAction(); + return true; + } + + return false; + } + + @Override + protected int getHoveredControlWidth() { + return getUnhoveredControlWidth(); + } + + @Override + public boolean canReset() { + return false; + } + + @Override + public boolean matchesSearch(String query) { + return super.matchesSearch(query) || buttonString.contains(query); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java new file mode 100644 index 0000000..d7d17d5 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java @@ -0,0 +1,157 @@ +package dev.isxander.yacl.gui.controllers; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * This controller renders a simple formatted {@link Component} + */ +public class BooleanController implements Controller { + + public static final Function ON_OFF_FORMATTER = (state) -> + state + ? CommonComponents.OPTION_ON + : CommonComponents.OPTION_OFF; + + public static final Function TRUE_FALSE_FORMATTER = (state) -> + state + ? Component.translatable("yacl.control.boolean.true") + : Component.translatable("yacl.control.boolean.false"); + + public static final Function YES_NO_FORMATTER = (state) -> + state + ? CommonComponents.GUI_YES + : CommonComponents.GUI_NO; + + private final Option option; + private final Function valueFormatter; + private final boolean coloured; + + /** + * Constructs a tickbox controller + * with the default value formatter of {@link BooleanController#ON_OFF_FORMATTER} + * + * @param option bound option + */ + public BooleanController(Option option) { + this(option, ON_OFF_FORMATTER, false); + } + + /** + * Constructs a tickbox controller + * with the default value formatter of {@link BooleanController#ON_OFF_FORMATTER} + * + * @param option bound option + * @param coloured value format is green or red depending on the state + */ + public BooleanController(Option option, boolean coloured) { + this(option, ON_OFF_FORMATTER, coloured); + } + + /** + * Constructs a tickbox controller + * + * @param option bound option + * @param valueFormatter format value into any {@link Component} + * @param coloured value format is green or red depending on the state + */ + public BooleanController(Option option, Function valueFormatter, boolean coloured) { + this.option = option; + this.valueFormatter = valueFormatter; + this.coloured = coloured; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * Value format is green or red depending on the state + */ + public boolean coloured() { + return coloured; + } + + /** + * {@inheritDoc} + */ + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new BooleanControllerElement(this, screen, widgetDimension); + } + + public static class BooleanControllerElement extends ControllerWidget { + public BooleanControllerElement(BooleanController control, YACLScreen screen, Dimension dim) { + super(control, screen, dim); + } + + @Override + protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY) || !isAvailable()) + return false; + + toggleSetting(); + return true; + } + + @Override + protected int getHoveredControlWidth() { + return getUnhoveredControlWidth(); + } + + public void toggleSetting() { + control.option().requestSet(!control.option().pendingValue()); + playDownSound(); + } + + @Override + protected Component getValueText() { + if (control.coloured()) { + return super.getValueText().copy().withStyle(control.option().pendingValue() ? ChatFormatting.GREEN : ChatFormatting.RED); + } + + return super.getValueText(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!isFocused()) { + return false; + } + + if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { + toggleSetting(); + return true; + } + + return false; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java new file mode 100644 index 0000000..3cbce40 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/ColorController.java @@ -0,0 +1,221 @@ +package dev.isxander.yacl.gui.controllers; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.api.utils.MutableDimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.string.IStringController; +import dev.isxander.yacl.gui.controllers.string.StringControllerElement; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import java.awt.*; +import java.util.List; + +/** + * A color controller that uses a hex color field as input. + */ +public class ColorController implements IStringController { + private final Option option; + private final boolean allowAlpha; + + /** + * Constructs a color controller with {@link ColorController#allowAlpha()} defaulting to false + * + * @param option bound option + */ + public ColorController(Option option) { + this(option, false); + } + + /** + * Constructs a color controller + * + * @param option bound option + * @param allowAlpha allows the color input to accept alpha values + */ + public ColorController(Option option, boolean allowAlpha) { + this.option = option; + this.allowAlpha = allowAlpha; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + public boolean allowAlpha() { + return allowAlpha; + } + + @Override + public String getString() { + return formatValue().getString(); + } + + @Override + public Component formatValue() { + MutableComponent text = Component.literal("#"); + text.append(Component.literal(toHex(option().pendingValue().getRed())).withStyle(ChatFormatting.RED)); + text.append(Component.literal(toHex(option().pendingValue().getGreen())).withStyle(ChatFormatting.GREEN)); + text.append(Component.literal(toHex(option().pendingValue().getBlue())).withStyle(ChatFormatting.BLUE)); + if (allowAlpha()) text.append(toHex(option().pendingValue().getAlpha())); + return text; + } + + private String toHex(int value) { + String hex = Integer.toString(value, 16).toUpperCase(); + if (hex.length() == 1) + hex = "0" + hex; + return hex; + } + + @Override + public void setFromString(String value) { + if (value.startsWith("#")) + value = value.substring(1); + + int red = Integer.parseInt(value.substring(0, 2), 16); + int green = Integer.parseInt(value.substring(2, 4), 16); + int blue = Integer.parseInt(value.substring(4, 6), 16); + + if (allowAlpha()) { + int alpha = Integer.parseInt(value.substring(6, 8), 16); + option().requestSet(new Color(red, green, blue, alpha)); + } else { + option().requestSet(new Color(red, green, blue)); + } + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new ColorControllerElement(this, screen, widgetDimension); + } + + public static class ColorControllerElement extends StringControllerElement { + private final ColorController colorController; + + protected MutableDimension colorPreviewDim; + + private final List allowedChars; + + public ColorControllerElement(ColorController control, YACLScreen screen, Dimension dim) { + super(control, screen, dim, true); + this.colorController = control; + this.allowedChars = ImmutableList.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); + } + + @Override + protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { + if (isHovered()) { + colorPreviewDim.move(-inputFieldBounds.width() - 5, 0); + super.drawValueText(matrices, mouseX, mouseY, delta); + } + + GuiComponent.fill(matrices, colorPreviewDim.x(), colorPreviewDim.y(), colorPreviewDim.xLimit(), colorPreviewDim.yLimit(), colorController.option().pendingValue().getRGB()); + drawOutline(matrices, colorPreviewDim.x(), colorPreviewDim.y(), colorPreviewDim.xLimit(), colorPreviewDim.yLimit(), 1, 0xFF000000); + } + + @Override + public void write(String string) { + if (string.startsWith("0x")) string = string.substring(2); + for (char chr : string.toCharArray()) { + if (!allowedChars.contains(Character.toLowerCase(chr))) { + return; + } + } + + if (caretPos == 0) + return; + + String trimmed = string.substring(0, Math.min(inputField.length() - caretPos, string.length())); + + if (modifyInput(builder -> builder.replace(caretPos, caretPos + trimmed.length(), trimmed))) { + caretPos += trimmed.length(); + setSelectionLength(); + updateControl(); + } + } + + @Override + protected void doBackspace() { + if (caretPos > 1) { + if (modifyInput(builder -> builder.setCharAt(caretPos - 1, '0'))) { + caretPos--; + updateControl(); + } + } + } + + @Override + protected void doDelete() { + if (caretPos >= 1) { + if (modifyInput(builder -> builder.setCharAt(caretPos, '0'))) { + updateControl(); + } + } + } + + @Override + protected boolean doCut() { + return false; + } + + @Override + protected boolean doCopy() { + return false; + } + + @Override + protected boolean doSelectAll() { + return false; + } + + protected void setSelectionLength() { + selectionLength = caretPos < inputField.length() && caretPos > 0 ? 1 : 0; + } + + @Override + protected int getDefaultCaretPos() { + return colorController.allowAlpha() ? 3 : 1; + } + + @Override + public void setDimension(Dimension dim) { + super.setDimension(dim); + + int previewSize = (dim.height() - getYPadding() * 2) / 2; + colorPreviewDim = Dimension.ofInt(dim.xLimit() - getXPadding() - previewSize, dim.centerY() - previewSize / 2, previewSize, previewSize); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + int prevSelectionLength = selectionLength; + selectionLength = 0; + if (super.keyPressed(keyCode, scanCode, modifiers)) { + caretPos = Math.max(1, caretPos); + setSelectionLength(); + return true; + } else selectionLength = prevSelectionLength; + return false; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)) { + caretPos = Math.max(1, caretPos); + setSelectionLength(); + return true; + } + return false; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java new file mode 100644 index 0000000..22c469d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java @@ -0,0 +1,170 @@ +package dev.isxander.yacl.gui.controllers; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.utils.GuiUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +public abstract class ControllerWidget> extends AbstractWidget { + protected final T control; + protected MultiLineLabel wrappedTooltip; + protected final YACLScreen screen; + + protected boolean focused = false; + protected boolean hovered = false; + + protected final Component modifiedOptionName; + protected final String optionNameString; + + public ControllerWidget(T control, YACLScreen screen, Dimension dim) { + super(dim); + this.control = control; + this.screen = screen; + control.option().addListener((opt, pending) -> updateTooltip()); + updateTooltip(); + this.modifiedOptionName = control.option().name().copy().withStyle(ChatFormatting.ITALIC); + this.optionNameString = control.option().name().getString().toLowerCase(); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + hovered = isMouseOver(mouseX, mouseY); + + Component name = control.option().changed() ? modifiedOptionName : control.option().name(); + Component shortenedName = Component.literal(GuiUtils.shortenString(name.getString(), textRenderer, getDimension().width() - getControlWidth() - getXPadding() - 7, "...")).setStyle(name.getStyle()); + + drawButtonRect(matrices, getDimension().x(), getDimension().y(), getDimension().xLimit(), getDimension().yLimit(), isHovered(), isAvailable()); + matrices.pushPose(); + matrices.translate(getDimension().x() + getXPadding(), getTextY(), 0); + textRenderer.drawShadow(matrices, shortenedName, 0, 0, getValueColor()); + matrices.popPose(); + + drawValueText(matrices, mouseX, mouseY, delta); + if (isHovered()) { + drawHoveredControl(matrices, mouseX, mouseY, delta); + } + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + if (hovered || focused) { + YACLScreen.renderMultilineTooltip(matrices, textRenderer, wrappedTooltip, getDimension().centerX(), getDimension().y() - 5, getDimension().yLimit() + 5, screen.width, screen.height); + } + } + + protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { + Component valueText = getValueText(); + matrices.pushPose(); + matrices.translate(getDimension().xLimit() - textRenderer.width(valueText) - getXPadding(), getTextY(), 0); + textRenderer.drawShadow(matrices, valueText, 0, 0, getValueColor()); + matrices.popPose(); + } + + private void updateTooltip() { + this.wrappedTooltip = MultiLineLabel.create(textRenderer, control.option().tooltip(), screen.width / 3 * 2 - 10); + } + + protected int getControlWidth() { + return isHovered() ? getHoveredControlWidth() : getUnhoveredControlWidth(); + } + + public boolean isHovered() { + return isAvailable() && (hovered || focused); + } + + protected abstract int getHoveredControlWidth(); + + protected int getUnhoveredControlWidth() { + return textRenderer.width(getValueText()); + } + + protected int getXPadding() { + return 5; + } + + protected int getYPadding() { + return 2; + } + + protected Component getValueText() { + return control.formatValue(); + } + + protected boolean isAvailable() { + return control.option().available(); + } + + protected int getValueColor() { + return isAvailable() ? -1 : inactiveColor; + } + + @Override + public boolean canReset() { + return true; + } + + protected void drawOutline(PoseStack matrices, int x1, int y1, int x2, int y2, int width, int color) { + GuiComponent.fill(matrices, x1, y1, x2, y1 + width, color); + GuiComponent.fill(matrices, x2, y1, x2 - width, y2, color); + GuiComponent.fill(matrices, x1, y2, x2, y2 - width, color); + GuiComponent.fill(matrices, x1, y1, x1 + width, y2, color); + } + + protected float getTextY() { + return getDimension().y() + getDimension().height() / 2f - textRenderer.lineHeight / 2f; + } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent focusNavigationEvent) { + if (!this.isAvailable()) + return null; + return !this.isFocused() ? ComponentPath.leaf(this) : null; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public void unfocus() { + this.focused = false; + } + + @Override + public boolean matchesSearch(String query) { + return optionNameString.contains(query.toLowerCase()); + } + + @Override + public NarrationPriority narrationPriority() { + return focused ? NarrationPriority.FOCUSED : isHovered() ? NarrationPriority.HOVERED : NarrationPriority.NONE; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, control.option().name()); + builder.add(NarratedElementType.HINT, control.option().tooltip()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java new file mode 100644 index 0000000..1dfae0b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java @@ -0,0 +1,193 @@ +package dev.isxander.yacl.gui.controllers; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.client.gui.ComponentPath; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.components.MultiLineLabel; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Simply renders some text as a label. + */ +public class LabelController implements Controller { + private final Option option; + /** + * Constructs a label controller + * + * @param option bound option + */ + public LabelController(Option option) { + this.option = option; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + @Override + public Component formatValue() { + return option().pendingValue(); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new LabelControllerElement(screen, widgetDimension); + } + + public class LabelControllerElement extends AbstractWidget { + private List wrappedText; + protected MultiLineLabel wrappedTooltip; + protected boolean focused; + + protected final YACLScreen screen; + + public LabelControllerElement(YACLScreen screen, Dimension dim) { + super(dim); + this.screen = screen; + option().addListener((opt, pending) -> updateTooltip()); + updateTooltip(); + updateText(); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + updateText(); + + float y = getDimension().y(); + for (FormattedCharSequence text : wrappedText) { + textRenderer.drawShadow(matrices, text, getDimension().x() + getXPadding(), y + getYPadding(), option().available() ? -1 : 0xFFA0A0A0); + y += textRenderer.lineHeight; + } + + if (isFocused()) { + GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().y() - 1, getDimension().xLimit() + 1, getDimension().y(), -1); + GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().y() - 1, getDimension().x(), getDimension().yLimit() + 1, -1); + GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().yLimit(), getDimension().xLimit() + 1, getDimension().yLimit() + 1, -1); + GuiComponent.fill(matrices, getDimension().xLimit(), getDimension().y() - 1, getDimension().xLimit() + 1, getDimension().yLimit() + 1, -1); + } + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + YACLScreen.renderMultilineTooltip(matrices, textRenderer, wrappedTooltip, getDimension().centerX(), getDimension().y() - 5, getDimension().yLimit() + 5, screen.width, screen.height); + + Style style = getStyle(mouseX, mouseY); + if (style != null && style.getHoverEvent() != null) { + HoverEvent hoverEvent = style.getHoverEvent(); + HoverEvent.ItemStackInfo itemStackContent = hoverEvent.getValue(HoverEvent.Action.SHOW_ITEM); + if (itemStackContent != null) { + ItemStack stack = itemStackContent.getItemStack(); + screen.renderTooltip(matrices, screen.getTooltipFromItem(stack), stack.getTooltipImage(), mouseX, mouseY); + } else { + HoverEvent.EntityTooltipInfo entityContent = hoverEvent.getValue(HoverEvent.Action.SHOW_ENTITY); + if (entityContent != null) { + if (this.client.options.advancedItemTooltips) { + screen.renderComponentTooltip(matrices, entityContent.getTooltipLines(), mouseX, mouseY); + } + } else { + Component text = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT); + if (text != null) { + MultiLineLabel multilineText = MultiLineLabel.create(textRenderer, text, getDimension().width()); + YACLScreen.renderMultilineTooltip(matrices, textRenderer, multilineText, getDimension().centerX(), getDimension().y(), getDimension().yLimit(), screen.width, screen.height); + } + } + } + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY)) + return false; + + Style style = getStyle((int) mouseX, (int) mouseY); + return screen.handleComponentClicked(style); + } + + protected Style getStyle(int mouseX, int mouseY) { + if (!getDimension().isPointInside(mouseX, mouseY)) + return null; + + int x = mouseX - getDimension().x(); + int y = mouseY - getDimension().y() - getYPadding(); + int line = y / textRenderer.lineHeight; + + if (x < 0 || x > getDimension().xLimit()) return null; + if (y < 0 || y > getDimension().yLimit()) return null; + if (line < 0 || line >= wrappedText.size()) return null; + + return textRenderer.getSplitter().componentStyleAtWidth(wrappedText.get(line), x); + } + + private int getXPadding() { + return 4; + } + + private int getYPadding() { + return 3; + } + + private void updateText() { + wrappedText = textRenderer.split(formatValue(), getDimension().width() - getXPadding() * 2); + setDimension(getDimension().withHeight(wrappedText.size() * textRenderer.lineHeight + getYPadding() * 2)); + } + + private void updateTooltip() { + this.wrappedTooltip = MultiLineLabel.create(textRenderer, option().tooltip(), screen.width / 3 * 2 - 10); + } + + @Override + public boolean matchesSearch(String query) { + return formatValue().getString().toLowerCase().contains(query.toLowerCase()); + } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent focusNavigationEvent) { + if (!option().available()) + return null; + return !this.isFocused() ? ComponentPath.leaf(this) : null; + } + + @Override + public boolean isFocused() { + return focused; + } + + @Override + public void setFocused(boolean focused) { + this.focused = focused; + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + builder.add(NarratedElementType.TITLE, formatValue()); + } + + @Override + public NarrationPriority narrationPriority() { + return NarrationPriority.FOCUSED; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java new file mode 100644 index 0000000..c3f674d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java @@ -0,0 +1,135 @@ +package dev.isxander.yacl.gui.controllers; + +import com.google.common.collect.ImmutableList; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.ListOption; +import dev.isxander.yacl.api.ListOptionEntry; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.TooltipButtonWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.client.gui.components.events.ContainerEventHandler; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class ListEntryWidget extends AbstractWidget implements ContainerEventHandler { + private final TooltipButtonWidget removeButton, moveUpButton, moveDownButton; + private final AbstractWidget entryWidget; + + private final ListOption listOption; + private final ListOptionEntry listOptionEntry; + + private final String optionNameString; + + private GuiEventListener focused; + private boolean dragging; + + public ListEntryWidget(YACLScreen screen, ListOptionEntry listOptionEntry, AbstractWidget entryWidget) { + super(entryWidget.getDimension().withHeight(Math.max(entryWidget.getDimension().height(), 20) - ((listOptionEntry.parentGroup().indexOf(listOptionEntry) == listOptionEntry.parentGroup().options().size() - 1) ? 0 : 2))); // -2 to remove the padding + this.listOptionEntry = listOptionEntry; + this.listOption = listOptionEntry.parentGroup(); + this.optionNameString = listOptionEntry.name().getString().toLowerCase(); + this.entryWidget = entryWidget; + + Dimension dim = entryWidget.getDimension(); + entryWidget.setDimension(dim.clone().move(20 * 2, 0).expand(-20 * 3, 0)); + + removeButton = new TooltipButtonWidget(screen, dim.xLimit() - 20, dim.y(), 20, 20, Component.literal("\u274c"), Component.translatable("yacl.list.remove"), btn -> { + listOption.removeEntry(listOptionEntry); + updateButtonStates(); + }); + + moveUpButton = new TooltipButtonWidget(screen, dim.x(), dim.y(), 20, 20, Component.literal("\u2191"), Component.translatable("yacl.list.move_up"), btn -> { + int index = listOption.indexOf(listOptionEntry) - 1; + if (index >= 0) { + listOption.removeEntry(listOptionEntry); + listOption.insertEntry(index, listOptionEntry); + updateButtonStates(); + } + }); + + moveDownButton = new TooltipButtonWidget(screen, dim.x() + 20, dim.y(), 20, 20, Component.literal("\u2193"), Component.translatable("yacl.list.move_down"), btn -> { + int index = listOption.indexOf(listOptionEntry) + 1; + if (index < listOption.options().size()) { + listOption.removeEntry(listOptionEntry); + listOption.insertEntry(index, listOptionEntry); + updateButtonStates(); + } + }); + + updateButtonStates(); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + updateButtonStates(); // update every render in case option becomes available/unavailable + + removeButton.setY(getDimension().y()); + moveUpButton.setY(getDimension().y()); + moveDownButton.setY(getDimension().y()); + entryWidget.setDimension(entryWidget.getDimension().withY(getDimension().y())); + + removeButton.render(matrices, mouseX, mouseY, delta); + moveUpButton.render(matrices, mouseX, mouseY, delta); + moveDownButton.render(matrices, mouseX, mouseY, delta); + entryWidget.render(matrices, mouseX, mouseY, delta); + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + removeButton.renderHoveredTooltip(matrices); + moveUpButton.renderHoveredTooltip(matrices); + moveDownButton.renderHoveredTooltip(matrices); + } + + protected void updateButtonStates() { + removeButton.active = listOption.available(); + moveUpButton.active = listOption.indexOf(listOptionEntry) > 0 && listOption.available(); + moveDownButton.active = listOption.indexOf(listOptionEntry) < listOption.options().size() - 1 && listOption.available(); + } + + @Override + public void unfocus() { + entryWidget.unfocus(); + } + + @Override + public void updateNarration(NarrationElementOutput builder) { + entryWidget.updateNarration(builder); + } + + @Override + public boolean matchesSearch(String query) { + return optionNameString.contains(query.toLowerCase()); + } + + @Override + public List children() { + return ImmutableList.of(moveUpButton, moveDownButton, entryWidget, removeButton); + } + + @Override + public boolean isDragging() { + return dragging; + } + + @Override + public void setDragging(boolean dragging) { + this.dragging = dragging; + } + + @Nullable + @Override + public GuiEventListener getFocused() { + return focused; + } + + @Override + public void setFocused(@Nullable GuiEventListener focused) { + this.focused = focused; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java new file mode 100644 index 0000000..3f615db --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java @@ -0,0 +1,120 @@ +package dev.isxander.yacl.gui.controllers; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.network.chat.Component; + +/** + * This controller renders a tickbox + */ +public class TickBoxController implements Controller { + private final Option option; + + /** + * Constructs a tickbox controller + * + * @param option bound option + */ + public TickBoxController(Option option) { + this.option = option; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return Component.empty(); + } + + /** + * {@inheritDoc} + */ + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new TickBoxControllerElement(this, screen, widgetDimension); + } + + public static class TickBoxControllerElement extends ControllerWidget { + public TickBoxControllerElement(TickBoxController control, YACLScreen screen, Dimension dim) { + super(control, screen, dim); + } + + @Override + protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { + int outlineSize = 10; + int outlineX1 = getDimension().xLimit() - getXPadding() - outlineSize; + int outlineY1 = getDimension().centerY() - outlineSize / 2; + int outlineX2 = getDimension().xLimit() - getXPadding(); + int outlineY2 = getDimension().centerY() + outlineSize / 2; + + int color = getValueColor(); + int shadowColor = multiplyColor(color, 0.25f); + + drawOutline(matrices, outlineX1 + 1, outlineY1 + 1, outlineX2 + 1, outlineY2 + 1, 1, shadowColor); + drawOutline(matrices, outlineX1, outlineY1, outlineX2, outlineY2, 1, color); + if (control.option().pendingValue()) { + GuiComponent.fill(matrices, outlineX1 + 3, outlineY1 + 3, outlineX2 - 1, outlineY2 - 1, shadowColor); + GuiComponent.fill(matrices, outlineX1 + 2, outlineY1 + 2, outlineX2 - 2, outlineY2 - 2, color); + } + } + + @Override + protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { + if (!isHovered()) + drawHoveredControl(matrices, mouseX, mouseY, delta); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY) || !isAvailable()) + return false; + + toggleSetting(); + return true; + } + + @Override + protected int getHoveredControlWidth() { + return 10; + } + + @Override + protected int getUnhoveredControlWidth() { + return 10; + } + + public void toggleSetting() { + control.option().requestSet(!control.option().pendingValue()); + playDownSound(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!focused) { + return false; + } + + if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { + toggleSetting(); + return true; + } + + return false; + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java new file mode 100644 index 0000000..ff20039 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java @@ -0,0 +1,60 @@ +package dev.isxander.yacl.gui.controllers.cycling; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.ControllerWidget; +import net.minecraft.client.gui.screens.Screen; + +public class CyclingControllerElement extends ControllerWidget> { + + public CyclingControllerElement(ICyclingController control, YACLScreen screen, Dimension dim) { + super(control, screen, dim); + } + + public void cycleValue(int increment) { + int targetIdx = control.getPendingValue() + increment; + if (targetIdx >= control.getCycleLength()) { + targetIdx -= control.getCycleLength(); + } else if (targetIdx < 0) { + targetIdx += control.getCycleLength(); + } + control.setPendingValue(targetIdx); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY) || (button != 0 && button != 1) || !isAvailable()) + return false; + + playDownSound(); + cycleValue(button == 1 || Screen.hasShiftDown() || Screen.hasControlDown() ? -1 : 1); + + return true; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!focused) + return false; + + switch (keyCode) { + case InputConstants.KEY_LEFT -> + cycleValue(-1); + case InputConstants.KEY_RIGHT -> + cycleValue(1); + case InputConstants.KEY_RETURN, InputConstants.KEY_SPACE, InputConstants.KEY_NUMPADENTER -> + cycleValue(Screen.hasControlDown() || Screen.hasShiftDown() ? -1 : 1); + default -> { + return false; + } + } + + return true; + } + + @Override + protected int getHoveredControlWidth() { + return getUnhoveredControlWidth(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java new file mode 100644 index 0000000..34f2cc9 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java @@ -0,0 +1,79 @@ +package dev.isxander.yacl.gui.controllers.cycling; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * A controller where once clicked, cycles through elements + * in the provided list. + */ +public class CyclingListController implements ICyclingController { + private final Option option; + private final Function valueFormatter; + private final ImmutableList values; + + /** + * Constructs a {@link CyclingListController}, with a default + * value formatter of {@link Object#toString()}. + * @param option option of which to bind the controller to + * @param values the values to cycle through + */ + public CyclingListController(Option option, Iterable values) { + this(option, values, value -> Component.literal(value.toString())); + } + + /** + * Constructs a {@link CyclingListController} + * @param option option of which to bind the controller to + * @param values the values to cycle through + * @param valueFormatter function of how to convert each value to a string to display + */ + public CyclingListController(Option option, Iterable values, Function valueFormatter) { + this.option = option; + this.valueFormatter = valueFormatter; + this.values = ImmutableList.copyOf(values); + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(int ordinal) { + option().requestSet(values.get(ordinal)); + } + + /** + * {@inheritDoc} + */ + @Override + public int getPendingValue() { + return values.indexOf(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public int getCycleLength() { + return values.size(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java new file mode 100644 index 0000000..ebd2cb6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java @@ -0,0 +1,60 @@ +package dev.isxander.yacl.gui.controllers.cycling; + +import dev.isxander.yacl.api.NameableEnum; +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; +import net.minecraft.util.OptionEnum; + +import java.util.Arrays; +import java.util.function.Function; + +/** + * Simple controller type that displays the enum on the right. + *

+ * Cycles forward with left click, cycles backward with right click or when shift is held + * + * @param enum type + */ +public class EnumController> extends CyclingListController { + public static > Function getDefaultFormatter() { + return value -> { + if (value instanceof NameableEnum nameableEnum) + return nameableEnum.getDisplayName(); + if (value instanceof OptionEnum translatableOption) + return translatableOption.getCaption(); + return Component.literal(value.toString()); + }; + } + + /** + * Constructs a cycling enum controller with a default value formatter and all values being available. + * The default value formatter first searches if the + * enum is a {@link NameableEnum} or {@link OptionEnum} else, just uses {@link Enum#toString()} + * + * @param option bound option + */ + public EnumController(Option option) { + this(option, getDefaultFormatter()); + } + + /** + * Constructs a cycling enum controller with all values being available. + * + * @param option bound option + * @param valueFormatter format the enum into any {@link Component} + */ + public EnumController(Option option, Function valueFormatter) { + this(option, valueFormatter, option.typeClass().getEnumConstants()); + } + + /** + * Constructs a cycling enum controller. + * + * @param option bound option + * @param valueFormatter format the enum into any {@link Component} + * @param availableValues all enum constants that can be cycled through + */ + public EnumController(Option option, Function valueFormatter, T[] availableValues) { + super(option, Arrays.asList(availableValues), valueFormatter); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java new file mode 100644 index 0000000..081b572 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java @@ -0,0 +1,38 @@ +package dev.isxander.yacl.gui.controllers.cycling; + +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; + +/** + * This interface simply generifies setting and getting of + * the pending value, using an ordinal so elements can cycle through + * without knowing the content. + */ +public interface ICyclingController extends Controller { + /** + * Sets the pending value to whatever corresponds to the ordinal + * @param ordinal index of element to set + */ + void setPendingValue(int ordinal); + + /** + * Gets the pending ordinal that corresponds to the actual value + * @return ordinal + */ + int getPendingValue(); + + /** + * Allows the element when it should wrap-around back to zeroth ordinal + */ + int getCycleLength(); + + /** + * {@inheritDoc} + */ + @Override + default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new CyclingControllerElement(this, screen, widgetDimension); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java new file mode 100644 index 0000000..12ce86b --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/package-info.java @@ -0,0 +1,12 @@ +/** + * This package contains all {@link dev.isxander.yacl.api.Controller} implementations + * + *

    + *
  • For numbers: {@link dev.isxander.yacl.gui.controllers.slider}
  • + *
  • For booleans: {@link dev.isxander.yacl.gui.controllers.TickBoxController}
  • + *
  • For lists/enums: {@link dev.isxander.yacl.gui.controllers.cycling}
  • + *
  • For strings: {@link dev.isxander.yacl.gui.controllers.string.StringController}
  • + *
  • For {@link dev.isxander.yacl.api.ButtonOption}: {@link dev.isxander.yacl.gui.controllers.ActionController}
  • + *
+ */ +package dev.isxander.yacl.gui.controllers; diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java new file mode 100644 index 0000000..8e044b1 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java @@ -0,0 +1,114 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.function.Function; + +/** + * {@link ISliderController} for doubles. + */ +public class DoubleSliderController implements ISliderController { + /** + * Formats doubles to two decimal places + */ + public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); + + private final Option option; + + private final double min, max, interval; + + private final Function valueFormatter; + + /** + * Constructs a {@link ISliderController} for doubles + * using the default value formatter {@link DoubleSliderController#DEFAULT_FORMATTER}. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + */ + public DoubleSliderController(Option option, double min, double max, double interval) { + this(option, min, max, interval, DEFAULT_FORMATTER); + } + + /** + * Constructs a {@link ISliderController} for doubles. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + * @param valueFormatter format the value into any {@link Component} + */ + public DoubleSliderController(Option option, double min, double max, double interval, Function valueFormatter) { + Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); + Validate.isTrue(interval > 0, "`interval` must be more than 0"); + Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); + + this.option = option; + this.min = min; + this.max = max; + this.interval = interval; + this.valueFormatter = valueFormatter; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public double interval() { + return interval; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet(value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } + +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java new file mode 100644 index 0000000..25f2206 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java @@ -0,0 +1,114 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.function.Function; + +/** + * {@link ISliderController} for floats. + */ +public class FloatSliderController implements ISliderController { + /** + * Formats floats to one decimal place + */ + public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,.1f", value).replaceAll("[\u00a0\u202F]", " ")); + + private final Option option; + + private final float min, max, interval; + + private final Function valueFormatter; + + /** + * Constructs a {@link ISliderController} for floats + * using the default value formatter {@link FloatSliderController#DEFAULT_FORMATTER}. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + */ + public FloatSliderController(Option option, float min, float max, float interval) { + this(option, min, max, interval, DEFAULT_FORMATTER); + } + + /** + * Constructs a {@link ISliderController} for floats. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + * @param valueFormatter format the value into any {@link Component} + */ + public FloatSliderController(Option option, float min, float max, float interval, Function valueFormatter) { + Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); + Validate.isTrue(interval > 0, "`interval` must be more than 0"); + Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); + + this.option = option; + this.min = min; + this.max = max; + this.interval = interval; + this.valueFormatter = valueFormatter; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public double interval() { + return interval; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((float) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } + +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java new file mode 100644 index 0000000..aa3c18f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java @@ -0,0 +1,54 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; + +/** + * Simple custom slider implementation that shifts the current value across when shown. + *

+ * For simplicity, {@link SliderControllerElement} works in doubles so each + * {@link ISliderController} must cast to double. This is to get around re-writing the element for every type. + */ +public interface ISliderController extends Controller { + /** + * Gets the minimum value for the slider + */ + double min(); + + /** + * Gets the maximum value for the slider + */ + double max(); + + /** + * Gets the interval (or step size) for the slider. + */ + double interval(); + + /** + * Gets the range of the slider. + */ + default double range() { + return max() - min(); + } + + /** + * Sets the {@link dev.isxander.yacl.api.Option}'s pending value + */ + void setPendingValue(double value); + + /** + * Gets the {@link dev.isxander.yacl.api.Option}'s pending value + */ + double pendingValue(); + + /** + * {@inheritDoc} + */ + @Override + default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new SliderControllerElement(this, screen, widgetDimension, min(), max(), interval()); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java new file mode 100644 index 0000000..4a68497 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.function.Function; + +/** + * {@link ISliderController} for integers. + */ +public class IntegerSliderController implements ISliderController { + public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,d", value).replaceAll("[\u00a0\u202F]", " ")); + + private final Option option; + + private final int min, max, interval; + + private final Function valueFormatter; + + /** + * Constructs a {@link ISliderController} for integers + * using the default value formatter {@link IntegerSliderController#DEFAULT_FORMATTER}. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + */ + public IntegerSliderController(Option option, int min, int max, int interval) { + this(option, min, max, interval, DEFAULT_FORMATTER); + } + + /** + * Constructs a {@link ISliderController} for integers. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + * @param valueFormatter format the value into any {@link Component} + */ + public IntegerSliderController(Option option, int min, int max, int interval, Function valueFormatter) { + Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); + Validate.isTrue(interval > 0, "`interval` must be more than 0"); + Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); + + this.option = option; + this.min = min; + this.max = max; + this.interval = interval; + this.valueFormatter = valueFormatter; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public double interval() { + return interval; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((int) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } + +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java new file mode 100644 index 0000000..681e7cf --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import dev.isxander.yacl.api.Option; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; + +import java.util.function.Function; + +/** + * {@link ISliderController} for longs. + */ +public class LongSliderController implements ISliderController { + public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,d", value).replaceAll("[\u00a0\u202F]", " ")); + + private final Option option; + + private final long min, max, interval; + + private final Function valueFormatter; + + /** + * Constructs a {@link ISliderController} for longs + * using the default value formatter {@link LongSliderController#DEFAULT_FORMATTER}. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + */ + public LongSliderController(Option option, long min, long max, long interval) { + this(option, min, max, interval, DEFAULT_FORMATTER); + } + + /** + * Constructs a {@link ISliderController} for longs. + * + * @param option bound option + * @param min minimum slider value + * @param max maximum slider value + * @param interval step size (or increments) for the slider + * @param valueFormatter format the value into any {@link Component} + */ + public LongSliderController(Option option, long min, long max, long interval, Function valueFormatter) { + Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); + Validate.isTrue(interval > 0, "`interval` must be more than 0"); + Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); + + this.option = option; + this.min = min; + this.max = max; + this.interval = interval; + this.valueFormatter = valueFormatter; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + /** + * {@inheritDoc} + */ + @Override + public Component formatValue() { + return valueFormatter.apply(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return max; + } + + /** + * {@inheritDoc} + */ + @Override + public double interval() { + return interval; + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((long) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } + +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java new file mode 100644 index 0000000..d00f3d7 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java @@ -0,0 +1,164 @@ +package dev.isxander.yacl.gui.controllers.slider; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.ControllerWidget; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.util.Mth; + +public class SliderControllerElement extends ControllerWidget> { + private final double min, max, interval; + + private float interpolation; + + private Dimension sliderBounds; + + private boolean mouseDown = false; + + public SliderControllerElement(ISliderController option, YACLScreen screen, Dimension dim, double min, double max, double interval) { + super(option, screen, dim); + this.min = min; + this.max = max; + this.interval = interval; + setDimension(dim); + } + + @Override + public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { + super.render(matrices, mouseX, mouseY, delta); + + calculateInterpolation(); + } + + @Override + protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { + // track + GuiComponent.fill(matrices, sliderBounds.x(), sliderBounds.centerY() - 1, sliderBounds.xLimit(), sliderBounds.centerY(), -1); + // track shadow + GuiComponent.fill(matrices, sliderBounds.x() + 1, sliderBounds.centerY(), sliderBounds.xLimit() + 1, sliderBounds.centerY() + 1, 0xFF404040); + + // thumb shadow + GuiComponent.fill(matrices, getThumbX() - getThumbWidth() / 2 + 1, sliderBounds.y() + 1, getThumbX() + getThumbWidth() / 2 + 1, sliderBounds.yLimit() + 1, 0xFF404040); + // thumb + GuiComponent.fill(matrices, getThumbX() - getThumbWidth() / 2, sliderBounds.y(), getThumbX() + getThumbWidth() / 2, sliderBounds.yLimit(), -1); + } + + @Override + protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { + matrices.pushPose(); + if (isHovered()) + matrices.translate(-(sliderBounds.width() + 6 + getThumbWidth() / 2f), 0, 0); + super.drawValueText(matrices, mouseX, mouseY, delta); + matrices.popPose(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isAvailable() || button != 0 || !sliderBounds.isPointInside((int) mouseX, (int) mouseY)) + return false; + + mouseDown = true; + + setValueFromMouse(mouseX); + return true; + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (!isAvailable() || button != 0 || !mouseDown) + return false; + + setValueFromMouse(mouseX); + return true; + } + + public void incrementValue(double amount) { + control.setPendingValue(Mth.clamp(control.pendingValue() + interval * amount, min, max)); + calculateInterpolation(); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + if (!isAvailable() || (!isMouseOver(mouseX, mouseY)) || (!Screen.hasShiftDown() && !Screen.hasControlDown())) + return false; + + incrementValue(amount); + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (isAvailable() && mouseDown) + playDownSound(); + mouseDown = false; + + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!focused) + return false; + + switch (keyCode) { + case InputConstants.KEY_LEFT -> incrementValue(-1); + case InputConstants.KEY_RIGHT -> incrementValue(1); + default -> { + return false; + } + } + + return true; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return super.isMouseOver(mouseX, mouseY) || mouseDown; + } + + protected void setValueFromMouse(double mouseX) { + double value = (mouseX - sliderBounds.x()) / sliderBounds.width() * control.range(); + control.setPendingValue(roundToInterval(value)); + calculateInterpolation(); + } + + protected double roundToInterval(double value) { + return Mth.clamp(min + (interval * Math.round(value / interval)), min, max); // extremely imprecise, requires clamping + } + + @Override + protected int getHoveredControlWidth() { + return sliderBounds.width() + getUnhoveredControlWidth() + 6 + getThumbWidth() / 2; + } + + protected void calculateInterpolation() { + interpolation = Mth.clamp((float) ((control.pendingValue() - control.min()) * 1 / control.range()), 0f, 1f); + } + + @Override + public void setDimension(Dimension dim) { + super.setDimension(dim); + int trackWidth = dim.width() / 3; + if (optionNameString.isEmpty()) + trackWidth = dim.width() / 2; + + sliderBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - getThumbWidth() / 2 - trackWidth, dim.centerY() - 5, trackWidth, 10); + } + + protected int getThumbX() { + return (int) (sliderBounds.x() + sliderBounds.width() * interpolation); + } + + protected int getThumbWidth() { + return 4; + } + + @Override + public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { + if (super.isMouseOver(mouseX, mouseY) || focused) + super.postRender(matrices, mouseX, mouseY, delta); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java new file mode 100644 index 0000000..bff0d57 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/slider/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains implementations of sliders for different number types + *

    + *
  • For doubles: {@link dev.isxander.yacl.gui.controllers.slider.DoubleSliderController}
  • + *
  • For floats: {@link dev.isxander.yacl.gui.controllers.slider.FloatSliderController}
  • + *
  • For integers: {@link dev.isxander.yacl.gui.controllers.slider.IntegerSliderController}
  • + *
  • For longs: {@link dev.isxander.yacl.gui.controllers.slider.LongSliderController}
  • + *
+ */ +package dev.isxander.yacl.gui.controllers.slider; diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java new file mode 100644 index 0000000..6a603d2 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java @@ -0,0 +1,44 @@ +package dev.isxander.yacl.gui.controllers.string; + +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.network.chat.Component; + +/** + * A controller that can be any type but can input and output a string. + */ +public interface IStringController extends Controller { + /** + * Gets the option's pending value as a string. + * + * @see Option#pendingValue() + */ + String getString(); + + /** + * Sets the option's pending value from a string. + * + * @see Option#requestSet(Object) + */ + void setFromString(String value); + + /** + * {@inheritDoc} + */ + @Override + default Component formatValue() { + return Component.literal(getString()); + } + + default boolean isInputValid(String input) { + return true; + } + + @Override + default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new StringControllerElement(this, screen, widgetDimension, true); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java new file mode 100644 index 0000000..85029b9 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringController.java @@ -0,0 +1,37 @@ +package dev.isxander.yacl.gui.controllers.string; + +import dev.isxander.yacl.api.Option; + +/** + * A custom text field implementation for strings. + */ +public class StringController implements IStringController { + private final Option option; + + /** + * Constructs a string controller + * + * @param option bound option + */ + public StringController(Option option) { + this.option = option; + } + + /** + * {@inheritDoc} + */ + @Override + public Option option() { + return option; + } + + @Override + public String getString() { + return option().pendingValue(); + } + + @Override + public void setFromString(String value) { + option().requestSet(value); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java new file mode 100644 index 0000000..cb2ad4e --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java @@ -0,0 +1,408 @@ +package dev.isxander.yacl.gui.controllers.string; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.ControllerWidget; +import dev.isxander.yacl.gui.utils.GuiUtils; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +public class StringControllerElement extends ControllerWidget> { + protected final boolean instantApply; + + protected String inputField; + protected Dimension inputFieldBounds; + protected boolean inputFieldFocused; + + protected int caretPos; + protected int selectionLength; + + protected int renderOffset; + + protected float ticks; + + private final Component emptyText; + + public StringControllerElement(IStringController control, YACLScreen screen, Dimension dim, boolean instantApply) { + super(control, screen, dim); + this.instantApply = instantApply; + inputField = control.getString(); + inputFieldFocused = false; + selectionLength = 0; + emptyText = Component.literal("Click to type...").withStyle(ChatFormatting.GRAY); + control.option().addListener((opt, val) -> inputField = control.getString()); + setDimension(dim); + } + + @Override + protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { + + } + + @Override + protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { + Component valueText = getValueText(); + if (!isHovered()) valueText = Component.literal(GuiUtils.shortenString(valueText.getString(), textRenderer, getMaxUnwrapLength(), "...")).setStyle(valueText.getStyle()); + + matrices.pushPose(); + int textX = getDimension().xLimit() - textRenderer.width(valueText) + renderOffset - getXPadding(); + matrices.translate(textX, getTextY(), 0); + GuiUtils.enableScissor(inputFieldBounds.x(), inputFieldBounds.y() - 2, inputFieldBounds.width() + 1, inputFieldBounds.height() + 4); + textRenderer.drawShadow(matrices, valueText, 0, 0, getValueColor()); + matrices.popPose(); + + if (isHovered()) { + ticks += delta; + + String text = getValueText().getString(); + + GuiComponent.fill(matrices, inputFieldBounds.x(), inputFieldBounds.yLimit(), inputFieldBounds.xLimit(), inputFieldBounds.yLimit() + 1, -1); + GuiComponent.fill(matrices, inputFieldBounds.x() + 1, inputFieldBounds.yLimit() + 1, inputFieldBounds.xLimit() + 1, inputFieldBounds.yLimit() + 2, 0xFF404040); + + if (inputFieldFocused || focused) { + if (caretPos > text.length()) + caretPos = text.length(); + + int caretX = textX + textRenderer.width(text.substring(0, caretPos)) - 1; + if (text.isEmpty()) + caretX = inputFieldBounds.x() + inputFieldBounds.width() / 2; + + if (ticks % 20 <= 10) { + GuiComponent.fill(matrices, caretX, inputFieldBounds.y(), caretX + 1, inputFieldBounds.yLimit(), -1); + } + + if (selectionLength != 0) { + int selectionX = textX + textRenderer.width(text.substring(0, caretPos + selectionLength)); + GuiComponent.fill(matrices, caretX, inputFieldBounds.y() - 1, selectionX, inputFieldBounds.yLimit(), 0x803030FF); + } + } + } + RenderSystem.disableScissor(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isAvailable() && getDimension().isPointInside((int) mouseX, (int) mouseY)) { + inputFieldFocused = true; + + if (!inputFieldBounds.isPointInside((int) mouseX, (int) mouseY)) { + caretPos = getDefaultCaretPos(); + } else { + // gets the appropriate caret position for where you click + int textX = (int) mouseX - (inputFieldBounds.xLimit() - textRenderer.width(getValueText())); + int pos = -1; + int currentWidth = 0; + for (char ch : inputField.toCharArray()) { + pos++; + int charLength = textRenderer.width(String.valueOf(ch)); + if (currentWidth + charLength / 2 > textX) { // if more than halfway past the characters select in front of that char + caretPos = pos; + break; + } else if (pos == inputField.length() - 1) { + // if we have reached the end and no matches, it must be the second half of the char so the last position + caretPos = pos + 1; + } + currentWidth += charLength; + } + + selectionLength = 0; + } + return true; + } else { + inputFieldFocused = false; + } + + return false; + } + + protected int getDefaultCaretPos() { + return inputField.length(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!inputFieldFocused) + return false; + + switch (keyCode) { + case InputConstants.KEY_ESCAPE, InputConstants.KEY_RETURN -> { + unfocus(); + return true; + } + case InputConstants.KEY_LEFT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(true); + selectionLength += caretPos - spaceChar; + caretPos = spaceChar; + } else if (caretPos > 0) { + caretPos--; + selectionLength += 1; + } + checkRenderOffset(); + } else { + if (caretPos > 0) { + if (selectionLength != 0) + caretPos += Math.min(selectionLength, 0); + else + caretPos--; + } + checkRenderOffset(); + selectionLength = 0; + } + + return true; + } + case InputConstants.KEY_RIGHT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(false); + selectionLength -= spaceChar - caretPos; + caretPos = spaceChar; + } else if (caretPos < inputField.length()) { + caretPos++; + selectionLength -= 1; + } + checkRenderOffset(); + } else { + if (caretPos < inputField.length()) { + if (selectionLength != 0) + caretPos += Math.max(selectionLength, 0); + else + caretPos++; + checkRenderOffset(); + } + selectionLength = 0; + } + + return true; + } + case InputConstants.KEY_BACKSPACE -> { + doBackspace(); + return true; + } + case InputConstants.KEY_DELETE -> { + doDelete(); + return true; + } + } + + if (Screen.isPaste(keyCode)) { + return doPaste(); + } else if (Screen.isCopy(keyCode)) { + return doCopy(); + } else if (Screen.isCut(keyCode)) { + return doCut(); + } else if (Screen.isSelectAll(keyCode)) { + return doSelectAll(); + } + + return false; + } + + protected boolean doPaste() { + this.write(client.keyboardHandler.getClipboard()); + return true; + } + + protected boolean doCopy() { + if (selectionLength != 0) { + client.keyboardHandler.setClipboard(getSelection()); + return true; + } + return false; + } + + protected boolean doCut() { + if (selectionLength != 0) { + client.keyboardHandler.setClipboard(getSelection()); + this.write(""); + return true; + } + return false; + } + + protected boolean doSelectAll() { + caretPos = inputField.length(); + checkRenderOffset(); + selectionLength = -caretPos; + return true; + } + + protected void checkRenderOffset() { + if (textRenderer.width(inputField) < getUnshiftedLength()) { + renderOffset = 0; + return; + } + + int textX = getDimension().xLimit() - textRenderer.width(inputField) - getXPadding(); + int caretX = textX + textRenderer.width(inputField.substring(0, caretPos)) - 1; + + int minX = getDimension().xLimit() - getXPadding() - getUnshiftedLength(); + int maxX = minX + getUnshiftedLength(); + + if (caretX + renderOffset < minX) { + renderOffset = minX - caretX; + } else if (caretX + renderOffset > maxX) { + renderOffset = maxX - caretX; + } + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (!inputFieldFocused) + return false; + + write(Character.toString(chr)); + + return true; + } + + protected void doBackspace() { + if (selectionLength != 0) { + write(""); + } else if (caretPos > 0) { + if (modifyInput(builder -> builder.deleteCharAt(caretPos - 1))) { + caretPos--; + checkRenderOffset(); + } + } + } + + protected void doDelete() { + if (selectionLength != 0) { + write(""); + } else if (caretPos < inputField.length()) { + modifyInput(builder -> builder.deleteCharAt(caretPos)); + } + } + + public void write(String string) { + if (selectionLength == 0) { + if (modifyInput(builder -> builder.insert(caretPos, string))) { + caretPos += string.length(); + checkRenderOffset(); + } + } else { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (modifyInput(builder -> builder.replace(start, end, string))) { + caretPos = start + string.length(); + selectionLength = 0; + checkRenderOffset(); + } + } + } + + public boolean modifyInput(Consumer consumer) { + StringBuilder temp = new StringBuilder(inputField); + consumer.accept(temp); + if (!control.isInputValid(temp.toString())) + return false; + inputField = temp.toString(); + if (instantApply) + updateControl(); + return true; + } + + public int getUnshiftedLength() { + if (optionNameString.isEmpty()) + return getDimension().width() - getXPadding() * 2; + return getDimension().width() / 8 * 5; + } + + public int getMaxUnwrapLength() { + if (optionNameString.isEmpty()) + return getDimension().width() - getXPadding() * 2; + return getDimension().width() / 2; + } + + public int getSelectionStart() { + return Math.min(caretPos, caretPos + selectionLength); + } + + public int getSelectionEnd() { + return Math.max(caretPos, caretPos + selectionLength); + } + + protected String getSelection() { + return inputField.substring(getSelectionStart(), getSelectionEnd()); + } + + protected int findSpaceIndex(boolean reverse) { + int i; + int fromIndex = caretPos; + if (reverse) { + if (caretPos > 0) + fromIndex -= 1; + i = this.inputField.lastIndexOf(" ", fromIndex); + + if (i == -1) i = 0; + } else { + if (caretPos < inputField.length()) + fromIndex += 1; + i = this.inputField.indexOf(" ", fromIndex); + + if (i == -1) i = inputField.length(); + } + + return i; + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + inputFieldFocused = focused; + } + + @Override + public void unfocus() { + super.unfocus(); + inputFieldFocused = false; + renderOffset = 0; + if (!instantApply) updateControl(); + } + + @Override + public void setDimension(Dimension dim) { + super.setDimension(dim); + + int width = Math.max(6, Math.min(textRenderer.width(getValueText()), getUnshiftedLength())); + inputFieldBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - width, dim.centerY() - textRenderer.lineHeight / 2, width, textRenderer.lineHeight); + } + + @Override + public boolean isHovered() { + return super.isHovered() || inputFieldFocused; + } + + protected void updateControl() { + control.setFromString(inputField); + } + + @Override + protected int getUnhoveredControlWidth() { + return !isHovered() ? Math.min(getHoveredControlWidth(), getMaxUnwrapLength()) : getHoveredControlWidth(); + } + + @Override + protected int getHoveredControlWidth() { + return Math.min(textRenderer.width(getValueText()), getUnshiftedLength()); + } + + @Override + protected Component getValueText() { + if (!inputFieldFocused && inputField.isEmpty()) + return emptyText; + + return instantApply || !inputFieldFocused ? control.formatValue() : Component.literal(inputField); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java new file mode 100644 index 0000000..df28241 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java @@ -0,0 +1,104 @@ +package dev.isxander.yacl.gui.controllers.string.number; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.gui.controllers.slider.DoubleSliderController; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class DoubleFieldController extends NumberFieldController { + private final double min, max; + + /** + * Constructs a double field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public DoubleFieldController(Option option, double min, double max, Function formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a double field controller. + * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public DoubleFieldController(Option option, double min, double max) { + this(option, min, max, DoubleSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a double field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public DoubleFieldController(Option option, Function formatter) { + this(option, -Double.MAX_VALUE, Double.MAX_VALUE, formatter); + } + + /** + * Constructs a double field controller. + * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public DoubleFieldController(Option option) { + this(option, -Double.MAX_VALUE, Double.MAX_VALUE, DoubleSliderController.DEFAULT_FORMATTER); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return String.valueOf(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet(value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java new file mode 100644 index 0000000..08084e6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java @@ -0,0 +1,104 @@ +package dev.isxander.yacl.gui.controllers.string.number; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class FloatFieldController extends NumberFieldController { + private final float min, max; + + /** + * Constructs a float field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public FloatFieldController(Option option, float min, float max, Function formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a float field controller. + * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public FloatFieldController(Option option, float min, float max) { + this(option, min, max, FloatSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a float field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public FloatFieldController(Option option, Function formatter) { + this(option, -Float.MAX_VALUE, Float.MAX_VALUE, formatter); + } + + /** + * Constructs a float field controller. + * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public FloatFieldController(Option option) { + this(option, -Float.MAX_VALUE, Float.MAX_VALUE, FloatSliderController.DEFAULT_FORMATTER); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return String.valueOf(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((float) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java new file mode 100644 index 0000000..726a590 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java @@ -0,0 +1,109 @@ +package dev.isxander.yacl.gui.controllers.string.number; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class IntegerFieldController extends NumberFieldController { + private final int min, max; + + /** + * Constructs a integer field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public IntegerFieldController(Option option, int min, int max, Function formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a integer field controller. + * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public IntegerFieldController(Option option, int min, int max) { + this(option, min, max, IntegerSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a integer field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public IntegerFieldController(Option option, Function formatter) { + this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, formatter); + } + + /** + * Constructs a integer field controller. + * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public IntegerFieldController(Option option) { + this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, IntegerSliderController.DEFAULT_FORMATTER); + } + + @Override + public boolean isInputValid(String input) { + return input.matches("\\d+|-|"); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return String.valueOf(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((int) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java new file mode 100644 index 0000000..d0c60b4 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java @@ -0,0 +1,109 @@ +package dev.isxander.yacl.gui.controllers.string.number; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.gui.controllers.slider.LongSliderController; +import net.minecraft.network.chat.Component; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class LongFieldController extends NumberFieldController { + private final long min, max; + + /** + * Constructs a long field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public LongFieldController(Option option, long min, long max, Function formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a long field controller. + * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public LongFieldController(Option option, long min, long max) { + this(option, min, max, LongSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a long field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public LongFieldController(Option option, Function formatter) { + this(option, -Long.MAX_VALUE, Long.MAX_VALUE, formatter); + } + + /** + * Constructs a long field controller. + * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public LongFieldController(Option option) { + this(option, -Long.MAX_VALUE, Long.MAX_VALUE, LongSliderController.DEFAULT_FORMATTER); + } + + @Override + public boolean isInputValid(String input) { + return input.matches("\\d+|-|"); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return String.valueOf(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((long) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java new file mode 100644 index 0000000..4240849 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java @@ -0,0 +1,69 @@ +package dev.isxander.yacl.gui.controllers.string.number; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.slider.ISliderController; +import dev.isxander.yacl.gui.controllers.string.IStringController; +import dev.isxander.yacl.gui.controllers.string.StringControllerElement; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +import java.text.DecimalFormatSymbols; +import java.util.function.Function; + +/** + * Controller that allows you to enter in numbers using a text field. + * + * @param number type + */ +public abstract class NumberFieldController implements ISliderController, IStringController { + private final Option option; + private final Function displayFormatter; + + public NumberFieldController(Option option, Function displayFormatter) { + this.option = option; + this.displayFormatter = displayFormatter; + } + + @Override + public Option option() { + return this.option; + } + + @Override + public void setFromString(String value) { + if (value.isEmpty() || value.equals(".") || value.equals("-")) value = "0"; + setPendingValue(Mth.clamp(Double.parseDouble(cleanupNumberString(value)), min(), max())); + } + + @Override + public double pendingValue() { + return option().pendingValue().doubleValue(); + } + + @Override + public boolean isInputValid(String input) { + return input.matches("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)|[.]||-"); + } + + @Override + public Component formatValue() { + return displayFormatter.apply(option().pendingValue()); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new StringControllerElement(this, screen, widgetDimension, false); + } + + protected String cleanupNumberString(String number) { + return number.replace(String.valueOf(DecimalFormatSymbols.getInstance().getGroupingSeparator()), ""); + } + + @Override + public double interval() { + return -1; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java new file mode 100644 index 0000000..86b9314 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains implementations of input fields for different number types + *
    + *
  • For doubles: {@link dev.isxander.yacl.gui.controllers.string.number.DoubleFieldController}
  • + *
  • For floats: {@link dev.isxander.yacl.gui.controllers.string.number.FloatFieldController}
  • + *
  • For integers: {@link dev.isxander.yacl.gui.controllers.string.number.IntegerFieldController}
  • + *
  • For longs: {@link dev.isxander.yacl.gui.controllers.string.number.LongFieldController}
  • + *
+ */ +package dev.isxander.yacl.gui.controllers.string.number; diff --git a/common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java b/common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java new file mode 100644 index 0000000..aa8bbaa --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/gui/utils/GuiUtils.java @@ -0,0 +1,41 @@ +package dev.isxander.yacl.gui.utils; + +import com.mojang.blaze3d.platform.Window; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +public class GuiUtils { + public static MutableComponent translatableFallback(String key, Component fallback) { + if (Language.getInstance().has(key)) + return Component.translatable(key); + return fallback.copy(); + } + + public static void enableScissor(int x, int y, int width, int height) { + Window window = Minecraft.getInstance().getWindow(); + double d = window.getGuiScale(); + RenderSystem.enableScissor((int)(x * d), (int)((window.getGuiScaledHeight() - y - height) * d), (int)(width * d), (int)(height * d)); + } + + public static String shortenString(String string, Font font, int maxWidth, String suffix) { + if (string.isEmpty()) + return string; + + boolean firstIter = true; + while (font.width(string) > maxWidth) { + string = string.substring(0, Math.max(string.length() - 1 - (firstIter ? 1 : suffix.length() + 1), 0)).trim(); + string += suffix; + + if (string.equals(suffix)) + break; + + firstIter = false; + } + + return string; + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java new file mode 100644 index 0000000..11da99e --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java @@ -0,0 +1,218 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +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.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; + +@ApiStatus.Internal +public final class ButtonOptionImpl implements ButtonOption { + private final Component name; + private final Component tooltip; + private final BiConsumer action; + private boolean available; + private final Controller> controller; + private final Binding> binding; + + public ButtonOptionImpl( + @NotNull Component name, + @Nullable Component tooltip, + @NotNull BiConsumer action, + boolean available, + @NotNull Function>> controlGetter + ) { + this.name = name; + this.tooltip = tooltip; + this.action = action; + this.available = available; + this.controller = controlGetter.apply(this); + this.binding = new EmptyBinderImpl(); + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @Override + public BiConsumer action() { + return action; + } + + @Override + public boolean available() { + return available; + } + + @Override + public void setAvailable(boolean available) { + this.available = available; + } + + @Override + public @NotNull Controller> controller() { + return controller; + } + + @Override + public @NotNull Binding> binding() { + return binding; + } + + @Override + public @NotNull Class> typeClass() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull ImmutableSet flags() { + return ImmutableSet.of(); + } + + @Override + public boolean changed() { + return false; + } + + @Override + public @NotNull BiConsumer pendingValue() { + throw new UnsupportedOperationException(); + } + + @Override + public void requestSet(BiConsumer value) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean applyValue() { + return false; + } + + @Override + public void forgetPendingValue() { + + } + + @Override + public void requestSetDefault() { + + } + + @Override + public boolean isPendingValueDefault() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(BiConsumer>, BiConsumer> changedListener) { + + } + + private static class EmptyBinderImpl implements Binding> { + @Override + public void setValue(BiConsumer value) { + + } + + @Override + public BiConsumer getValue() { + throw new UnsupportedOperationException(); + } + + @Override + public BiConsumer defaultValue() { + throw new UnsupportedOperationException(); + } + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component name; + private final List tooltipLines = new ArrayList<>(); + private boolean available = true; + private Function>> controlGetter; + private BiConsumer action; + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + Validate.notNull(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + @Override + public Builder action(@NotNull BiConsumer action) { + Validate.notNull(action, "`action` cannot be null"); + + this.action = action; + return this; + } + + @Override + @Deprecated + public Builder action(@NotNull Consumer action) { + Validate.notNull(action, "`action` cannot be null"); + + this.action = (screen, button) -> action.accept(screen); + return this; + } + + @Override + public Builder available(boolean available) { + this.available = available; + return this; + } + + @Override + public Builder controller(@NotNull Function>> control) { + Validate.notNull(control, "`control` cannot be null"); + + this.controlGetter = control; + return this; + } + + @Override + public ButtonOption build() { + Validate.notNull(name, "`name` must not be null when building `Option`"); + Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); + Validate.notNull(action, "`action` must not be null when building `Option`"); + + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Component line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new ButtonOptionImpl(name, concatenatedTooltip, action, available, controlGetter); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java new file mode 100644 index 0000000..2d39eb9 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java @@ -0,0 +1,137 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.api.ListOption; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionGroup; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@ApiStatus.Internal +public final class ConfigCategoryImpl implements ConfigCategory { + private final Component name; + private final ImmutableList groups; + private final Component tooltip; + + public ConfigCategoryImpl(Component name, ImmutableList groups, Component tooltip) { + this.name = name; + this.groups = groups; + this.tooltip = tooltip; + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public @NotNull ImmutableList groups() { + return groups; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component name; + + private final List> rootOptions = new ArrayList<>(); + private final List groups = new ArrayList<>(); + + private final List tooltipLines = new ArrayList<>(); + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + @Override + public Builder option(@NotNull Option option) { + Validate.notNull(option, "`option` must not be null"); + + if (option instanceof ListOption listOption) { + YACLConstants.LOGGER.warn("Adding list option as an option is not supported! Rerouting to group!"); + return group(listOption); + } + + this.rootOptions.add(option); + return this; + } + + @Override + public Builder options(@NotNull Collection> options) { + Validate.notNull(options, "`options` must not be null"); + + if (options.stream().anyMatch(ListOption.class::isInstance)) + throw new UnsupportedOperationException("List options must not be added as an option but a group!"); + + this.rootOptions.addAll(options); + return this; + } + + @Override + public Builder group(@NotNull OptionGroup group) { + Validate.notNull(group, "`group` must not be null"); + + this.groups.add(group); + return this; + } + + @Override + public Builder groups(@NotNull Collection groups) { + Validate.notEmpty(groups, "`groups` must not be empty"); + + this.groups.addAll(groups); + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + @Override + public ConfigCategory build() { + Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); + + List combinedGroups = new ArrayList<>(); + combinedGroups.add(new OptionGroupImpl(Component.empty(), Component.empty(), ImmutableList.copyOf(rootOptions), false, true)); + combinedGroups.addAll(groups); + + Validate.notEmpty(combinedGroups, "at least one option must be added to build `ConfigCategory`"); + + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Component line : tooltipLines) { + if (line.getContents() == ComponentContents.EMPTY) + continue; + + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new ConfigCategoryImpl(name, ImmutableList.copyOf(combinedGroups), concatenatedTooltip); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/GenericBindingImpl.java b/common/src/main/java/dev/isxander/yacl/impl/GenericBindingImpl.java new file mode 100644 index 0000000..0d668c6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/GenericBindingImpl.java @@ -0,0 +1,35 @@ +package dev.isxander.yacl.impl; + +import dev.isxander.yacl.api.Binding; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class GenericBindingImpl implements Binding { + private final T def; + private final Supplier getter; + private final Consumer setter; + + public GenericBindingImpl(T def, Supplier getter, Consumer setting) { + this.def = def; + this.getter = getter; + this.setter = setting; + } + + + @Override + public void setValue(T value) { + setter.accept(value); + } + + @Override + public T getValue() { + return getter.get(); + } + + @Override + public T defaultValue() { + return def; + } + +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java new file mode 100644 index 0000000..732a373 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java @@ -0,0 +1,154 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.controllers.LabelController; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.function.BiConsumer; + +@ApiStatus.Internal +public final class LabelOptionImpl implements LabelOption { + private final Component label; + private final Component name = Component.literal("Label Option"); + private final Component tooltip = Component.empty(); + private final LabelController labelController; + private final Binding binding; + + public LabelOptionImpl(Component label) { + this.label = label; + this.labelController = new LabelController(this); + this.binding = Binding.immutable(label); + } + + @Override + public @NotNull Component label() { + return label; + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @Override + public @NotNull Controller controller() { + return labelController; + } + + @Override + public @NotNull Binding binding() { + return binding; + } + + @Override + public boolean available() { + return true; + } + + @Override + public void setAvailable(boolean available) { + throw new UnsupportedOperationException("Label options cannot be disabled."); + } + + @Override + public @NotNull Class typeClass() { + return Component.class; + } + + @Override + public @NotNull ImmutableSet flags() { + return ImmutableSet.of(); + } + + @Override + public boolean changed() { + return false; + } + + @Override + public @NotNull Component pendingValue() { + return label; + } + + @Override + public void requestSet(Component value) { + + } + + @Override + public boolean applyValue() { + return false; + } + + @Override + public void forgetPendingValue() { + + } + + @Override + public void requestSetDefault() { + + } + + @Override + public boolean isPendingValueDefault() { + return true; + } + + @Override + public boolean canResetToDefault() { + return false; + } + + @Override + public void addListener(BiConsumer, Component> changedListener) { + + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private final List lines = new ArrayList<>(); + + @Override + public Builder line(@NotNull Component line) { + Validate.notNull(line, "`line` must not be null"); + + this.lines.add(line); + return this; + } + + @Override + public Builder lines(@NotNull Collection lines) { + this.lines.addAll(lines); + return this; + } + + @Override + public LabelOption build() { + MutableComponent text = Component.empty(); + Iterator iterator = lines.iterator(); + while (iterator.hasNext()) { + text.append(iterator.next()); + + if (iterator.hasNext()) + text.append("\n"); + } + + return new LabelOptionImpl(text); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java new file mode 100644 index 0000000..c15efe6 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java @@ -0,0 +1,149 @@ +package dev.isxander.yacl.impl; + +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.ListEntryWidget; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +@ApiStatus.Internal +public final class ListOptionEntryImpl implements ListOptionEntry { + private final ListOptionImpl group; + + private T value; + + private final Binding binding; + private final Controller controller; + + ListOptionEntryImpl(ListOptionImpl group, T initialValue, @NotNull Function, Controller> controlGetter) { + this.group = group; + this.value = initialValue; + this.binding = new EntryBinding(); + this.controller = new EntryController<>(controlGetter.apply(this), this); + } + + @Override + public @NotNull Component name() { + return Component.empty(); + } + + @Override + public @NotNull Component tooltip() { + return Component.empty(); + } + + @Override + public @NotNull Controller controller() { + return controller; + } + + @Override + public @NotNull Binding binding() { + return binding; + } + + @Override + public boolean available() { + return parentGroup().available(); + } + + @Override + public void setAvailable(boolean available) { + + } + + @Override + public ListOption parentGroup() { + return group; + } + + @Override + public boolean changed() { + return false; + } + + @Override + public @NotNull T pendingValue() { + return value; + } + + @Override + public void requestSet(T value) { + binding.setValue(value); + } + + @Override + public boolean applyValue() { + return false; + } + + @Override + public void forgetPendingValue() { + + } + + @Override + public void requestSetDefault() { + + } + + @Override + public boolean isPendingValueDefault() { + return false; + } + + @Override + public boolean canResetToDefault() { + return false; + } + + @Override + public void addListener(BiConsumer, T> changedListener) { + + } + + /** + * Open in case mods need to find the real controller type. + */ + @ApiStatus.Internal + public record EntryController(Controller controller, ListOptionEntryImpl entry) implements Controller { + @Override + public Option option() { + return controller.option(); + } + + @Override + public Component formatValue() { + return controller.formatValue(); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { + return new ListEntryWidget(screen, entry, controller.provideWidget(screen, widgetDimension)); + } + } + + private class EntryBinding implements Binding { + @Override + public void setValue(T newValue) { + value = newValue; + group.callListeners(); + } + + @Override + public T getValue() { + return value; + } + + @Override + public T defaultValue() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java new file mode 100644 index 0000000..f47493c --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java @@ -0,0 +1,338 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.api.*; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@ApiStatus.Internal +public final class ListOptionImpl implements ListOption { + private final Component name; + private final Component tooltip; + private final Binding> binding; + private final T initialValue; + private final List> entries; + private final boolean collapsed; + private boolean available; + private final Class typeClass; + private final ImmutableSet flags; + private final EntryFactory entryFactory; + private final List>, List>> listeners; + private final List refreshListeners; + + public ListOptionImpl(@NotNull Component name, @NotNull Component tooltip, @NotNull Binding> binding, @NotNull T initialValue, @NotNull Class typeClass, @NotNull Function, Controller> controllerFunction, ImmutableSet flags, boolean collapsed, boolean available, Collection>, List>> listeners) { + this.name = name; + this.tooltip = tooltip; + this.binding = binding; + this.initialValue = initialValue; + this.entryFactory = new EntryFactory(controllerFunction); + this.entries = createEntries(binding().getValue()); + this.collapsed = collapsed; + this.typeClass = typeClass; + this.flags = flags; + this.available = available; + this.listeners = new ArrayList<>(); + this.listeners.addAll(listeners); + this.refreshListeners = new ArrayList<>(); + callListeners(); + } + + @Override + public @NotNull Component name() { + return this.name; + } + + @Override + public @NotNull Component tooltip() { + return this.tooltip; + } + + @Override + public @NotNull ImmutableList> options() { + return ImmutableList.copyOf(entries); + } + + @Override + public @NotNull Controller> controller() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Binding> binding() { + return binding; + } + + @Override + public @NotNull Class> typeClass() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Class elementTypeClass() { + return typeClass; + } + + @Override + public boolean collapsed() { + return collapsed; + } + + @Override + public @NotNull ImmutableSet flags() { + return flags; + } + + @Override + public @NotNull ImmutableList pendingValue() { + return ImmutableList.copyOf(entries.stream().map(Option::pendingValue).toList()); + } + + @Override + public void insertEntry(int index, ListOptionEntry entry) { + entries.add(index, (ListOptionEntry) entry); + onRefresh(); + } + + @Override + public ListOptionEntry insertNewEntryToTop() { + ListOptionEntry newEntry = entryFactory.create(initialValue); + entries.add(0, newEntry); + onRefresh(); + return newEntry; + } + + @Override + public void removeEntry(ListOptionEntry entry) { + if (entries.remove(entry)) + onRefresh(); + } + + @Override + public int indexOf(ListOptionEntry entry) { + return entries.indexOf(entry); + } + + @Override + public void requestSet(List value) { + entries.clear(); + entries.addAll(createEntries(value)); + onRefresh(); + } + + @Override + public boolean changed() { + return !binding().getValue().equals(pendingValue()); + } + + @Override + public boolean applyValue() { + if (changed()) { + binding().setValue(pendingValue()); + return true; + } + return false; + } + + @Override + public void forgetPendingValue() { + requestSet(binding().getValue()); + } + + @Override + public void requestSetDefault() { + requestSet(binding().defaultValue()); + } + + @Override + public boolean isPendingValueDefault() { + return binding().defaultValue().equals(pendingValue()); + } + + @Override + public boolean available() { + return available; + } + + @Override + public void setAvailable(boolean available) { + this.available = available; + } + + @Override + public void addListener(BiConsumer>, List> changedListener) { + this.listeners.add(changedListener); + } + + @Override + public void addRefreshListener(Runnable changedListener) { + this.refreshListeners.add(changedListener); + } + + @Override + public boolean isRoot() { + return false; + } + + private List> createEntries(Collection values) { + return values.stream().map(entryFactory::create).collect(Collectors.toList()); + } + + void callListeners() { + List pendingValue = pendingValue(); + this.listeners.forEach(listener -> listener.accept(this, pendingValue)); + } + + private void onRefresh() { + refreshListeners.forEach(Runnable::run); + callListeners(); + } + + private class EntryFactory { + private final Function, Controller> controllerFunction; + + private EntryFactory(Function, Controller> controllerFunction) { + this.controllerFunction = controllerFunction; + } + + public ListOptionEntry create(T initialValue) { + return new ListOptionEntryImpl<>(ListOptionImpl.this, initialValue, controllerFunction); + } + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component name = Component.empty(); + private final List tooltipLines = new ArrayList<>(); + private Function, Controller> controllerFunction; + private Binding> binding = null; + private final Set flags = new HashSet<>(); + private T initialValue; + private boolean collapsed = false; + private boolean available = true; + private final List>, List>> listeners = new ArrayList<>(); + private final Class typeClass; + + public BuilderImpl(Class typeClass) { + this.typeClass = typeClass; + } + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` must not be null"); + + this.name = name; + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + @Override + public Builder initial(@NotNull T initialValue) { + Validate.notNull(initialValue, "`initialValue` cannot be empty"); + + this.initialValue = initialValue; + return this; + } + + @Override + public Builder controller(@NotNull Function, Controller> control) { + Validate.notNull(control, "`control` cannot be null"); + + this.controllerFunction = control; + return this; + } + + @Override + public Builder binding(@NotNull Binding> binding) { + Validate.notNull(binding, "`binding` cannot be null"); + + this.binding = binding; + return this; + } + + @Override + public Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + this.binding = Binding.generic(def, getter, setter); + return this; + } + + @Override + public Builder available(boolean available) { + this.available = available; + return this; + } + + @Override + public Builder flag(@NotNull OptionFlag... flag) { + Validate.notNull(flag, "`flag` must not be null"); + + this.flags.addAll(Arrays.asList(flag)); + return this; + } + + @Override + public Builder flags(@NotNull Collection flags) { + Validate.notNull(flags, "`flags` must not be null"); + + this.flags.addAll(flags); + return this; + } + + @Override + public Builder collapsed(boolean collapsible) { + this.collapsed = collapsible; + return this; + } + + @Override + public Builder listener(@NotNull BiConsumer>, List> listener) { + this.listeners.add(listener); + return this; + } + + @Override + public Builder listeners(@NotNull Collection>, List>> listeners) { + this.listeners.addAll(listeners); + return this; + } + + @Override + public ListOption build() { + Validate.notNull(controllerFunction, "`controller` must not be null"); + Validate.notNull(binding, "`binding` must not be null"); + Validate.notNull(initialValue, "`initialValue` must not be null"); + + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Component line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new ListOptionImpl<>(name, concatenatedTooltip, binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java new file mode 100644 index 0000000..113aefc --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java @@ -0,0 +1,129 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.ListOption; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionGroup; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@ApiStatus.Internal +public final class OptionGroupImpl implements OptionGroup { + private final @NotNull Component name; + private final @NotNull Component tooltip; + private final ImmutableList> options; + private final boolean collapsed; + private final boolean isRoot; + + public OptionGroupImpl(@NotNull Component name, @NotNull Component tooltip, ImmutableList> options, boolean collapsed, boolean isRoot) { + this.name = name; + this.tooltip = tooltip; + this.options = options; + this.collapsed = collapsed; + this.isRoot = isRoot; + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @Override + public @NotNull ImmutableList> options() { + return options; + } + + @Override + public boolean collapsed() { + return collapsed; + } + + @Override + public boolean isRoot() { + return isRoot; + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component name = Component.empty(); + private final List tooltipLines = new ArrayList<>(); + private final List> options = new ArrayList<>(); + private boolean collapsed = false; + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` must not be null"); + + this.name = name; + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + @Override + public Builder option(@NotNull Option option) { + Validate.notNull(option, "`option` must not be null"); + + if (option instanceof ListOption) + throw new UnsupportedOperationException("List options must not be added as an option but a group!"); + + this.options.add(option); + return this; + } + + @Override + public Builder options(@NotNull Collection> options) { + Validate.notEmpty(options, "`options` must not be empty"); + + if (options.stream().anyMatch(ListOption.class::isInstance)) + throw new UnsupportedOperationException("List options must not be added as an option but a group!"); + + this.options.addAll(options); + return this; + } + + @Override + public Builder collapsed(boolean collapsible) { + this.collapsed = collapsible; + return this; + } + + @Override + public OptionGroup build() { + Validate.notEmpty(options, "`options` must not be empty to build `OptionGroup`"); + + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Component line : tooltipLines) { + if (line.getContents() == ComponentContents.EMPTY) + continue; + + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new OptionGroupImpl(name, concatenatedTooltip, ImmutableList.copyOf(options), collapsed, false); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java new file mode 100644 index 0000000..4b65d56 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java @@ -0,0 +1,303 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableSet; +import dev.isxander.yacl.api.Binding; +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.OptionFlag; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentContents; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +@ApiStatus.Internal +public final class OptionImpl implements Option { + private final Component name; + private Component tooltip; + private final Controller controller; + private final Binding binding; + private boolean available; + + private final ImmutableSet flags; + + private final Class typeClass; + + private T pendingValue; + + private final List, T>> listeners; + + public OptionImpl( + @NotNull Component name, + @Nullable Function tooltipGetter, + @NotNull Function, Controller> controlGetter, + @NotNull Binding binding, + boolean available, + ImmutableSet flags, + @NotNull Class typeClass, + @NotNull Collection, T>> listeners + ) { + this.name = name; + this.binding = binding; + this.available = available; + this.flags = flags; + this.typeClass = typeClass; + this.listeners = new ArrayList<>(listeners); + this.controller = controlGetter.apply(this); + + addListener((opt, pending) -> tooltip = tooltipGetter.apply(pending)); + requestSet(binding().getValue()); + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @Override + public @NotNull Controller controller() { + return controller; + } + + @Override + public @NotNull Binding binding() { + return binding; + } + + @Override + public boolean available() { + return available; + } + + @Override + public void setAvailable(boolean available) { + this.available = available; + } + + @Override + public @NotNull Class typeClass() { + return typeClass; + } + + @Override + public @NotNull ImmutableSet flags() { + return flags; + } + + @Override + public boolean changed() { + return !binding().getValue().equals(pendingValue); + } + + @Override + public @NotNull T pendingValue() { + return pendingValue; + } + + @Override + public void requestSet(T value) { + pendingValue = value; + listeners.forEach(listener -> listener.accept(this, pendingValue)); + } + + @Override + public boolean applyValue() { + if (changed()) { + binding().setValue(pendingValue); + return true; + } + return false; + } + + @Override + public void forgetPendingValue() { + requestSet(binding().getValue()); + } + + @Override + public void requestSetDefault() { + requestSet(binding().defaultValue()); + } + + @Override + public boolean isPendingValueDefault() { + return binding().defaultValue().equals(pendingValue()); + } + + @Override + public void addListener(BiConsumer, T> changedListener) { + this.listeners.add(changedListener); + } + + @ApiStatus.Internal + public static class BuilderImpl implements Builder { + private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED); + + private final List> tooltipGetters = new ArrayList<>(); + + private Function, Controller> controlGetter; + + private Binding binding; + + private boolean available = true; + + private boolean instant = false; + + private final Set flags = new HashSet<>(); + + private final Class typeClass; + + private final List, T>> listeners = new ArrayList<>(); + + public BuilderImpl(Class typeClass) { + this.typeClass = typeClass; + } + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + @Override + public Builder tooltip(@NotNull Function tooltipGetter) { + Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); + + this.tooltipGetters.add(tooltipGetter); + return this; + } + + @Override + @SafeVarargs + @Deprecated + public final Builder tooltip(@NotNull Function... tooltipGetter) { + Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); + + this.tooltipGetters.addAll(List.of(tooltipGetter)); + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + var tooltipFunctions = Arrays.stream(tooltips) + .map(t -> (Function) opt -> t) + .toList(); + + this.tooltipGetters.addAll(tooltipFunctions); + return this; + } + + @Override + public Builder controller(@NotNull Function, Controller> control) { + Validate.notNull(control, "`control` cannot be null"); + + this.controlGetter = control; + return this; + } + + @Override + public Builder binding(@NotNull Binding binding) { + Validate.notNull(binding, "`binding` cannot be null"); + + this.binding = binding; + return this; + } + + @Override + public Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) { + Validate.notNull(def, "`def` must not be null"); + Validate.notNull(getter, "`getter` must not be null"); + Validate.notNull(setter, "`setter` must not be null"); + + this.binding = Binding.generic(def, getter, setter); + return this; + } + + @Override + public Builder available(boolean available) { + this.available = available; + return this; + } + + @Override + public Builder flag(@NotNull OptionFlag... flag) { + Validate.notNull(flag, "`flag` must not be null"); + + this.flags.addAll(Arrays.asList(flag)); + return this; + } + + @Override + public Builder flags(@NotNull Collection flags) { + Validate.notNull(flags, "`flags` must not be null"); + + this.flags.addAll(flags); + return this; + } + + @Override + public Builder instant(boolean instant) { + this.instant = instant; + return this; + } + + @Override + public Builder listener(@NotNull BiConsumer, T> listener) { + this.listeners.add(listener); + return this; + } + + @Override + public Builder listeners(@NotNull Collection, T>> listeners) { + this.listeners.addAll(listeners); + return this; + } + + @Override + public Option build() { + Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); + Validate.notNull(binding, "`binding` must not be null when building `Option`"); + Validate.isTrue(!instant || flags.isEmpty(), "instant application does not support option flags"); + + Function concatenatedTooltipGetter = value -> { + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Function line : tooltipGetters) { + Component lineComponent = line.apply(value); + + if (lineComponent.getContents() == ComponentContents.EMPTY) + continue; + + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(lineComponent); + } + + return concatenatedTooltip; + }; + + if (instant) { + listeners.add((opt, pendingValue) -> opt.applyValue()); + } + + return new OptionImpl<>(name, concatenatedTooltipGetter, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java b/common/src/main/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java new file mode 100644 index 0000000..4e41a8f --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java @@ -0,0 +1,99 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.OptionGroup; +import dev.isxander.yacl.api.PlaceholderCategory; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +@ApiStatus.Internal +public final class PlaceholderCategoryImpl implements PlaceholderCategory { + private final Component name; + private final BiFunction screen; + private final Component tooltip; + + public PlaceholderCategoryImpl(Component name, BiFunction screen, Component tooltip) { + this.name = name; + this.screen = screen; + this.tooltip = tooltip; + } + + @Override + public @NotNull ImmutableList groups() { + return ImmutableList.of(); + } + + @Override + public @NotNull Component name() { + return name; + } + + @Override + public BiFunction screen() { + return screen; + } + + @Override + public @NotNull Component tooltip() { + return tooltip; + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component name; + + private final List tooltipLines = new ArrayList<>(); + + private BiFunction screenFunction; + + @Override + public Builder name(@NotNull Component name) { + Validate.notNull(name, "`name` cannot be null"); + + this.name = name; + return this; + } + + @Override + public Builder tooltip(@NotNull Component... tooltips) { + Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); + + tooltipLines.addAll(List.of(tooltips)); + return this; + } + + @Override + public Builder screen(@NotNull BiFunction screenFunction) { + Validate.notNull(screenFunction, "`screenFunction` cannot be null"); + + this.screenFunction = screenFunction; + return this; + } + + @Override + public PlaceholderCategory build() { + Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); + + MutableComponent concatenatedTooltip = Component.empty(); + boolean first = true; + for (Component line : tooltipLines) { + if (!first) concatenatedTooltip.append("\n"); + first = false; + + concatenatedTooltip.append(line); + } + + return new PlaceholderCategoryImpl(name, screenFunction, concatenatedTooltip); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java b/common/src/main/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java new file mode 100644 index 0000000..3c3cad2 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java @@ -0,0 +1,122 @@ +package dev.isxander.yacl.impl; + +import com.google.common.collect.ImmutableList; +import dev.isxander.yacl.api.ConfigCategory; +import dev.isxander.yacl.api.PlaceholderCategory; +import dev.isxander.yacl.api.YetAnotherConfigLib; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.impl.utils.YACLConstants; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; + +@ApiStatus.Internal +public final class YetAnotherConfigLibImpl implements YetAnotherConfigLib { + private final Component title; + private final ImmutableList categories; + private final Runnable saveFunction; + private final Consumer initConsumer; + + private boolean generated = false; + + public YetAnotherConfigLibImpl(Component title, ImmutableList categories, Runnable saveFunction, Consumer initConsumer) { + this.title = title; + this.categories = categories; + this.saveFunction = saveFunction; + this.initConsumer = initConsumer; + } + + @Override + public Screen generateScreen(Screen parent) { + if (generated) + throw new UnsupportedOperationException("To prevent memory leaks, you should only generate a Screen once per instance. Please re-build the instance to generate another GUI."); + + YACLConstants.LOGGER.info("Generating YACL screen"); + generated = true; + return new YACLScreen(this, parent); + } + + @Override + public Component title() { + return title; + } + + @Override + public ImmutableList categories() { + return categories; + } + + @Override + public Runnable saveFunction() { + return saveFunction; + } + + @Override + public Consumer initConsumer() { + return initConsumer; + } + + @ApiStatus.Internal + public static final class BuilderImpl implements Builder { + private Component title; + private final List categories = new ArrayList<>(); + private Runnable saveFunction = () -> {}; + private Consumer initConsumer = screen -> {}; + + @Override + public Builder title(@NotNull Component title) { + Validate.notNull(title, "`title` cannot be null"); + + this.title = title; + return this; + } + + @Override + public Builder category(@NotNull ConfigCategory category) { + Validate.notNull(category, "`category` cannot be null"); + + this.categories.add(category); + return this; + } + + @Override + public Builder categories(@NotNull Collection categories) { + Validate.notNull(categories, "`categories` cannot be null"); + + this.categories.addAll(categories); + return this; + } + + @Override + public Builder save(@NotNull Runnable saveFunction) { + Validate.notNull(saveFunction, "`saveFunction` cannot be null"); + + this.saveFunction = saveFunction; + return this; + } + + @Override + public Builder screenInit(@NotNull Consumer initConsumer) { + Validate.notNull(initConsumer, "`initConsumer` cannot be null"); + + this.initConsumer = initConsumer; + return this; + } + + @Override + public YetAnotherConfigLib build() { + Validate.notNull(title, "`title must not be null to build `YetAnotherConfigLib`"); + Validate.notEmpty(categories, "`categories` must not be empty to build `YetAnotherConfigLib`"); + Validate.isTrue(!categories.stream().allMatch(category -> category instanceof PlaceholderCategory), "At least one regular category is required to build `YetAnotherConfigLib`"); + + return new YetAnotherConfigLibImpl(title, ImmutableList.copyOf(categories), saveFunction, initConsumer); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java b/common/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java new file mode 100644 index 0000000..6c7508d --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java @@ -0,0 +1,115 @@ +package dev.isxander.yacl.impl.utils; + +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.api.utils.MutableDimension; + +public class DimensionIntegerImpl implements MutableDimension { + private int x, y; + private int width, height; + + public DimensionIntegerImpl(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public Integer x() { + return x; + } + + @Override + public Integer y() { + return y; + } + + @Override + public Integer width() { + return width; + } + + @Override + public Integer height() { + return height; + } + + @Override + public Integer xLimit() { + return x + width; + } + + @Override + public Integer yLimit() { + return y + height; + } + + @Override + public Integer centerX() { + return x + width / 2; + } + + @Override + public Integer centerY() { + return y + height / 2; + } + + @Override + public boolean isPointInside(Integer x, Integer y) { + return x >= x() && x <= xLimit() && y >= y() && y <= yLimit(); + } + + @Override + public MutableDimension clone() { + return new DimensionIntegerImpl(x, y, width, height); + } + + @Override public MutableDimension setX(Integer x) { this.x = x; return this; } + @Override public MutableDimension setY(Integer y) { this.y = y; return this; } + @Override public MutableDimension setWidth(Integer width) { this.width = width; return this; } + @Override public MutableDimension setHeight(Integer height) { this.height = height; return this; } + + @Override + public Dimension withX(Integer x) { + return clone().setX(x); + } + + @Override + public Dimension withY(Integer y) { + return clone().setY(y); + } + + @Override + public Dimension withWidth(Integer width) { + return clone().setWidth(width); + } + + @Override + public Dimension withHeight(Integer height) { + return clone().setHeight(height); + } + + @Override + public MutableDimension move(Integer x, Integer y) { + this.x += x; + this.y += y; + return this; + } + + @Override + public MutableDimension expand(Integer width, Integer height) { + this.width += width; + this.height += height; + return this; + } + + @Override + public Dimension moved(Integer x, Integer y) { + return clone().move(x, y); + } + + @Override + public Dimension expanded(Integer width, Integer height) { + return clone().expand(width, height); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java b/common/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java new file mode 100644 index 0000000..3d382d4 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java @@ -0,0 +1,8 @@ +package dev.isxander.yacl.impl.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class YACLConstants { + public static final Logger LOGGER = LoggerFactory.getLogger("YetAnotherConfigLib"); +} diff --git a/common/src/main/java/dev/isxander/yacl/mixin/AbstractSelectionListMixin.java b/common/src/main/java/dev/isxander/yacl/mixin/AbstractSelectionListMixin.java new file mode 100644 index 0000000..1b53e96 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/mixin/AbstractSelectionListMixin.java @@ -0,0 +1,25 @@ +package dev.isxander.yacl.mixin; + +import net.minecraft.client.gui.components.AbstractSelectionList; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.List; + +@Mixin(AbstractSelectionList.class) +public abstract class AbstractSelectionListMixin> { + @Shadow public abstract List children(); + + /** + * Mojang use the field access of children to get max index to loop through keyboard navigation to find the next entry. + * YACL modifies these children() method to filter out hidden entries, so we need to redirect the field access to the + * method, so we don't get ArrayIndexOutOfBoundsException. + */ + @Redirect(method = "nextEntry(Lnet/minecraft/client/gui/navigation/ScreenDirection;Ljava/util/function/Predicate;Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry;)Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry;", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/components/AbstractSelectionList;children:Ljava/util/List;", opcode = Opcodes.GETFIELD)) + private List modifyChildrenCall(AbstractSelectionList instance) { + return children(); + } +} diff --git a/common/src/main/java/dev/isxander/yacl/mixin/OptionInstanceAccessor.java b/common/src/main/java/dev/isxander/yacl/mixin/OptionInstanceAccessor.java new file mode 100644 index 0000000..4eea9a9 --- /dev/null +++ b/common/src/main/java/dev/isxander/yacl/mixin/OptionInstanceAccessor.java @@ -0,0 +1,13 @@ +package dev.isxander.yacl.mixin; + +import net.minecraft.client.OptionInstance; +import org.jetbrains.annotations.ApiStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@ApiStatus.Internal +@Mixin(OptionInstance.class) +public interface OptionInstanceAccessor { + @Accessor + T getInitialValue(); +} diff --git a/common/src/main/resources/architectury.common.json b/common/src/main/resources/architectury.common.json new file mode 100644 index 0000000..0cb21dd --- /dev/null +++ b/common/src/main/resources/architectury.common.json @@ -0,0 +1,3 @@ +{ + "accessWidener": "yacl.accesswidener" +} \ No newline at end of file diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/el_gr.json b/common/src/main/resources/assets/yet_another_config_lib/lang/el_gr.json new file mode 100644 index 0000000..b7bc2d2 --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/el_gr.json @@ -0,0 +1,23 @@ +{ + "yacl.control.boolean.true": "Ναι", + "yacl.control.boolean.false": "Όχι", + + "yacl.control.action.execute": "ΕΚΤΕΛΕΣΗ", + + "yacl.gui.save": "Αποθήκευση Αλλαγών", + "yacl.gui.save.tooltip": "Μονιμοποιήει τις αλλαγές.", + "yacl.gui.finished.tooltip": "Κλείνει το GUI.", + "yacl.gui.cancel.tooltip": "Ξεχνά τις ενεργές αλλαγές και κλείνει το μενού.", + "yacl.gui.reset.tooltip": "Επαναφέρει όλες τις επιλογές στις προεπιλογές τους. (Μπορεί να ανερεθε)", + "yacl.gui.undo": "Επαναφορά", + "yacl.gui.undo.tooltip": "Επαναφέρει όλες τις ενεργές επιλογές στις προεπιλογές τους.", + "yacl.gui.fail_apply": "Αποτυχία εφαρμογής", + "yacl.gui.fail_apply.tooltip": "Δημιουργήθηκε ένα σφάλμα και οι αλλαγές δεν μπόρεσαν να εφαρμοστούν.", + "yacl.gui.save_before_exit": "Αποθήκευση πριν το κλείσιμο!", + "yacl.gui.save_before_exit.tooltip": "Αποθήκευσε ή ακύρωσε για να βγεις απ' το μενού." + + "yacl.restart.title": "Η ρύθμιση απαιτεί επανεκκήνιση!", + "yacl.restart.message": "Μία ή παραπάνω επιλογές προϋποθέτουν επανεκκήνηση το παιχνιδιού για να εφαρμοστούν.", + "yacl.restart.yes": "Κλείσιμο του Minecraft", + "yacl.restart.no": "Αγνόησε το για τώρα" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/en_us.json b/common/src/main/resources/assets/yet_another_config_lib/lang/en_us.json new file mode 100644 index 0000000..32621e9 --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/en_us.json @@ -0,0 +1,29 @@ +{ + "yacl.control.boolean.true": "True", + "yacl.control.boolean.false": "False", + + "yacl.control.action.execute": "EXECUTE", + + "yacl.gui.save": "Save Changes", + "yacl.gui.save.tooltip": "Makes the changes made permanent.", + "yacl.gui.finished.tooltip": "Closes the GUI.", + "yacl.gui.cancel.tooltip": "Forgets pending changes and closes the GUI.", + "yacl.gui.reset.tooltip": "Resets all options to default. (This is reversible!)", + "yacl.gui.undo": "Undo", + "yacl.gui.undo.tooltip": "Reverts all options back to what they were before editing.", + "yacl.gui.fail_apply": "Failed to apply", + "yacl.gui.fail_apply.tooltip": "There was an error and the changes couldn't be applied.", + "yacl.gui.save_before_exit": "Save before exiting!", + "yacl.gui.save_before_exit.tooltip": "Save or cancel to exit the GUI.", + + "yacl.list.move_up": "Move up", + "yacl.list.move_down": "Move down", + "yacl.list.remove": "Remove", + "yacl.list.add_top": "New entry", + "yacl.list.empty": "List is empty", + + "yacl.restart.title": "Config requires restart!", + "yacl.restart.message": "One or more options needs you to restart the game to apply the changes.", + "yacl.restart.yes": "Close Minecraft", + "yacl.restart.no": "Ignore" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/et_ee.json b/common/src/main/resources/assets/yet_another_config_lib/lang/et_ee.json new file mode 100644 index 0000000..5f5274a --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/et_ee.json @@ -0,0 +1,18 @@ +{ + "yacl.control.boolean.true": "Tõene", + "yacl.control.boolean.false": "Väär", + + "yacl.control.action.execute": "KÄIVITA", + + "yacl.gui.save": "Salvesta muudatused", + "yacl.gui.save.tooltip": "Teeb tehtud muudatused püsivaks.", + "yacl.gui.finished.tooltip": "Sulgeb liidese.", + "yacl.gui.cancel.tooltip": "Unustab tehtud muudatused ja sulgeb liidese.", + "yacl.gui.reset.tooltip": "Lähtestab kõik valikud vaikeväärtustele. (Seda saab tagasi võtta!)", + "yacl.gui.undo": "Võta tagasi", + "yacl.gui.undo.tooltip": "Lähtestab kõik valikud muutmise-eelsetele väärtustele.", + "yacl.gui.fail_apply": "Rakendamine ebaõnnestus", + "yacl.gui.fail_apply.tooltip": "Esines viga ja muudatusi ei saadud rakendada.", + "yacl.gui.save_before_exit": "Salvesta enne väljumist!", + "yacl.gui.save_before_exit.tooltip": "Liidesest väljumiseks salvesta või loobu." +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/fr_fr.json b/common/src/main/resources/assets/yet_another_config_lib/lang/fr_fr.json new file mode 100644 index 0000000..bc069cf --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/fr_fr.json @@ -0,0 +1,29 @@ +{ + "yacl.control.boolean.true": "Vrai", + "yacl.control.boolean.false": "Faux", + + "yacl.control.action.execute": "EXÉCUTION", + + "yacl.gui.save": "Sauvegarder les modifications", + "yacl.gui.save.tooltip": "Rend les changements effectués permanents.", + "yacl.gui.finished.tooltip": "Ferme la superposition.", + "yacl.gui.cancel.tooltip": "Oublie les changements en cours et ferme la superposition.", + "yacl.gui.reset.tooltip": "Réinitialise toutes les options par défaut. (Réversible !)", + "yacl.gui.undo": "Annuler", + "yacl.gui.undo.tooltip": "Rétablit toutes les options telles qu'elles étaient avant l'édition.", + "yacl.gui.fail_apply": "Échec de l'application", + "yacl.gui.fail_apply.tooltip": "Il y a eu une erreur et les changements n'ont pas pu être appliqués.", + "yacl.gui.save_before_exit": "Sauvegardez avant de quitter !", + "yacl.gui.save_before_exit.tooltip": "Sauvegardez ou annulez pour quitter la superposition.", + + "yacl.list.move_up": "Monter en haut", + "yacl.list.move_down": "Monter en bas", + "yacl.list.remove": "Retirer", + "yacl.list.add_top": "Nouvelle entrée", + "yacl.list.empty": "La liste est vide", + + "yacl.restart.title": "La configuration nécessite un redémarrage !", + "yacl.restart.message": "Une ou plusieurs options nécessitent que vous redémarriez le jeu pour appliquer les changements.", + "yacl.restart.yes": "Fermer Minecraft", + "yacl.restart.no": "Ignorer" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/pl_pl.json b/common/src/main/resources/assets/yet_another_config_lib/lang/pl_pl.json new file mode 100644 index 0000000..49074ea --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/pl_pl.json @@ -0,0 +1,23 @@ +{ + "yacl.control.boolean.true": "Tak", + "yacl.control.boolean.false": "Nie", + + "yacl.control.action.execute": "WYKONAJ", + + "yacl.gui.save": "Zapisz zmiany", + "yacl.gui.save.tooltip": "Sprawia, że wprowadzone zmiany są trwałe.", + "yacl.gui.finished.tooltip": "Zamyka GUI.", + "yacl.gui.cancel.tooltip": "Zapomina oczekujące zmiany i zamyka GUI.", + "yacl.gui.reset.tooltip": "Resetuje wszystkie opcje do wartości domyślnych. (To jest odwracalne!)", + "yacl.gui.undo": "Cofnij", + "yacl.gui.undo.tooltip": "Przywraca wszystkie opcje do stanu sprzed edycji.", + "yacl.gui.fail_apply": "Nie udało się zastosować", + "yacl.gui.fail_apply.tooltip": "Wystąpił błąd i nie udało się zastosować zmian.", + "yacl.gui.save_before_exit": "Zapisz przed wyjściem!", + "yacl.gui.save_before_exit.tooltip": "Zapisz lub anuluj, aby wyjść z GUI.", + + "yacl.restart.title": "Konfiguracja wymaga restartu!", + "yacl.restart.message": "Jedna lub więcej opcji wymaga ponownego uruchomienia gry, aby zastosować zmiany.", + "yacl.restart.yes": "Zamknij Minecrafta", + "yacl.restart.no": "Ignoruj" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/pt_br.json b/common/src/main/resources/assets/yet_another_config_lib/lang/pt_br.json new file mode 100644 index 0000000..9d4ef8d --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/pt_br.json @@ -0,0 +1,18 @@ +{ + "yacl.control.boolean.true": "Verdadeiro", + "yacl.control.boolean.false": "Falso", + + "yacl.control.action.execute": "EXECUTAR", + + "yacl.gui.save": "Salvar Mudanças", + "yacl.gui.save.tooltip": "Faz as mudanças serem permanentes.", + "yacl.gui.finished.tooltip": "Fecha o GUI.", + "yacl.gui.cancel.tooltip": "Esquece as mudanças pendentes e fecha o GUI.", + "yacl.gui.reset.tooltip": "Reinicia todas as opções para o valor padrão. (Isso é irreversível!)", + "yacl.gui.undo": "Desfazer", + "yacl.gui.undo.tooltip": "Reverte todas as opções como elas estavam antes de serem editadas.", + "yacl.gui.fail_apply": "Falha na aplicação", + "yacl.gui.fail_apply.tooltip": "Houve um erro e as mudanças não puderam ser aplicadas.", + "yacl.gui.save_before_exit": "Salve antes de sair!", + "yacl.gui.save_before_exit.tooltip": "Salve ou calcele para sair do GUI." +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/ru_ru.json b/common/src/main/resources/assets/yet_another_config_lib/lang/ru_ru.json new file mode 100644 index 0000000..5725d34 --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/ru_ru.json @@ -0,0 +1,24 @@ +{ + "yacl.control.boolean.true": "§atrue", + "yacl.control.boolean.false": "§cfalse", + + "yacl.control.action.execute": "Выполнить", + + "yacl.gui.save": "Сохранить", + "yacl.gui.save.tooltip": "Сохранить изменения до следующего редактирования.", + "yacl.gui.finished.tooltip": "Закрыть меню.", + "yacl.gui.cancel": "Назад", + "yacl.gui.cancel.tooltip": "Отменить изменения и закрыть настройки.", + "yacl.gui.reset.tooltip": "Сбросить все настройки до значений по умолчанию (их можно восстановить).", + "yacl.gui.undo": "Отменить", + "yacl.gui.undo.tooltip": "Вернуть все настройки к состоянию, в котором они были до изменений.", + "yacl.gui.fail_apply": "Не удалось сохранить", + "yacl.gui.fail_apply.tooltip": "Возникла ошибка; изменения невозможно применить.", + "yacl.gui.save_before_exit": "Сохраните перед закрытием", + "yacl.gui.save_before_exit.tooltip": "Сохраните или отмените изменения, чтобы закрыть настройки.", + + "yacl.restart.title": "Настройки требуют перезагрузки.", + "yacl.restart.message": "Одна или несколько настроек требует перезапуска игры для применения изменений.", + "yacl.restart.yes": "Закрыть Minecraft", + "yacl.restart.no": "Игнорировать" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/sl_si.json b/common/src/main/resources/assets/yet_another_config_lib/lang/sl_si.json new file mode 100644 index 0000000..743dd4d --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/sl_si.json @@ -0,0 +1,22 @@ +{ + "yacl.control.boolean.true": "Vklopljeno", + "yacl.control.boolean.false": "Izklopljeno", + + "yacl.control.action.execute": "POŽENI", + + "yacl.gui.save": "Shrani spremembe", + "yacl.gui.save.tooltip": "Uvede trajne spremembe.", + "yacl.gui.finished.tooltip": "Zapre meni.", + "yacl.gui.cancel.tooltip": "Zavrže neshranjene spremembe in zapre meni.", + "yacl.gui.reset.tooltip": "Ponastavi vse možnosti na privzete. (To se da razveljaviti!)", + "yacl.gui.undo": "Razveljavi", + "yacl.gui.undo.tooltip": "Ponastavi vse možnosti na take kot pred spreminjanjem.", + "yacl.gui.fail_apply": "Napaka pri uveljavljanju", + "yacl.gui.fail_apply.tooltip": "Prišlo je do napake pri uveljavljanju sprememb.", + "yacl.gui.save_before_exit": "Shrani pred izhodom!", + "yacl.gui.save_before_exit.tooltip": "Shrani ali prekliči za izhod iz menija.", + "yacl.restart.title": "Nastavitve potrebujejo ponovni zagon!", + "yacl.restart.message": "Vsaj ena izmed nastavitev potrebuje ponovni zagon igre za uveljavitev.", + "yacl.restart.yes": "Zapri Minecraft", + "yacl.restart.no": "Prezri" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/zh_cn.json b/common/src/main/resources/assets/yet_another_config_lib/lang/zh_cn.json new file mode 100644 index 0000000..9307c9b --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/zh_cn.json @@ -0,0 +1,29 @@ +{ + "yacl.control.boolean.true": "是", + "yacl.control.boolean.false": "否", + + "yacl.control.action.execute": "执行", + + "yacl.gui.save": "保存更改", + "yacl.gui.save.tooltip": "永久保存所做更改。", + "yacl.gui.finished.tooltip": "关闭 GUI。", + "yacl.gui.cancel.tooltip": "忽略犹豫不决的更改然后关闭 GUI。", + "yacl.gui.reset.tooltip": "将所有选项重置为默认值。(这是可逆的!)", + "yacl.gui.undo": "撤销", + "yacl.gui.undo.tooltip": "将所有选项恢复到编辑前的状态。", + "yacl.gui.fail_apply": "应用失败", + "yacl.gui.fail_apply.tooltip": "有一个错误以至于更改不能被应用。", + "yacl.gui.save_before_exit": "退出前保存!", + "yacl.gui.save_before_exit.tooltip": "保存或取消以退出 GUI。", + + "yacl.list.move_up": "上移", + "yacl.list.move_down": "下移", + "yacl.list.remove": "移除", + "yacl.list.add_top": "新条目", + "yacl.list.empty": "列表为空", + + "yacl.restart.title": "配置需要重新启动!", + "yacl.restart.message": "一个或多个选项需要你重新启动游戏来应用这些更改。", + "yacl.restart.yes": "关闭 Minecraft", + "yacl.restart.no": "忽略" +} diff --git a/common/src/main/resources/assets/yet_another_config_lib/lang/zh_tw.json b/common/src/main/resources/assets/yet_another_config_lib/lang/zh_tw.json new file mode 100644 index 0000000..67d05a7 --- /dev/null +++ b/common/src/main/resources/assets/yet_another_config_lib/lang/zh_tw.json @@ -0,0 +1,23 @@ +{ + "yacl.control.boolean.true": "是", + "yacl.control.boolean.false": "否", + + "yacl.control.action.execute": "執行", + + "yacl.gui.save": "儲存變更", + "yacl.gui.save.tooltip": "儲存你的變更。", + "yacl.gui.finished.tooltip": "關閉介面。", + "yacl.gui.cancel.tooltip": "取消變更並關閉介面。", + "yacl.gui.reset.tooltip": "重設所有選項到預設。(這可以復原!)", + "yacl.gui.undo": "復原", + "yacl.gui.undo.tooltip": "將所有選項恢復成編輯前的狀態。", + "yacl.gui.fail_apply": "套用失敗", + "yacl.gui.fail_apply.tooltip": "發生錯誤,無法套用變更。", + "yacl.gui.save_before_exit": "在離開時儲存!", + "yacl.gui.save_before_exit.tooltip": "儲存或是取消並離開介面。", + + "yacl.restart.title": "變更設定需要重開遊戲!", + "yacl.restart.message": "一個或多個選項需要你重開遊戲才能套用變更。", + "yacl.restart.yes": "關閉 Minecraft", + "yacl.restart.no": "忽略" +} diff --git a/common/src/main/resources/yacl-128x.png b/common/src/main/resources/yacl-128x.png new file mode 100644 index 0000000..c86981c Binary files /dev/null and b/common/src/main/resources/yacl-128x.png differ diff --git a/common/src/main/resources/yacl.accesswidener b/common/src/main/resources/yacl.accesswidener new file mode 100644 index 0000000..2188ea5 --- /dev/null +++ b/common/src/main/resources/yacl.accesswidener @@ -0,0 +1,6 @@ +accessWidener v2 named + +extendable method net/minecraft/client/gui/components/AbstractSelectionList children ()Ljava/util/List; +extendable method net/minecraft/client/gui/components/AbstractSelectionList getEntryAtPosition (DD)Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry; +accessible class net/minecraft/client/gui/components/AbstractSelectionList$Entry +extendable method net/minecraft/client/gui/components/AbstractButton getTextureY ()I \ No newline at end of file diff --git a/common/src/main/resources/yacl.mixins.json b/common/src/main/resources/yacl.mixins.json new file mode 100644 index 0000000..6151c26 --- /dev/null +++ b/common/src/main/resources/yacl.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "dev.isxander.yacl.mixin", + "compatibilityLevel": "JAVA_17", + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "AbstractSelectionListMixin", + "OptionInstanceAccessor" + ] +} diff --git a/fabric/.gitignore b/fabric/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/fabric/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts new file mode 100644 index 0000000..6a4d2d0 --- /dev/null +++ b/fabric/build.gradle.kts @@ -0,0 +1,158 @@ +import org.gradle.jvm.tasks.Jar + +plugins { + alias(libs.plugins.architectury.loom) + alias(libs.plugins.shadow) + alias(libs.plugins.unified.publishing) +} + +architectury { + platformSetupLoomIde() + fabric() +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(project(":common").loom.accessWidenerPath) +} + +val common by configurations.registering +val shadowCommon by configurations.registering +configurations.compileClasspath.get().extendsFrom(common.get()) +configurations["developmentFabric"].extendsFrom(common.get()) + +val minecraftVersion = libs.versions.minecraft.get() + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + modImplementation(libs.fabric.loader) + + listOf( + "fabric-resource-loader-v0" + ).forEach { modApi(fabricApi.module(it, libs.versions.fabric.api.get())) } + modApi(libs.mod.menu) + + "common"(project(path = ":common", configuration = "namedElements")) { isTransitive = false } + "shadowCommon"(project(path = ":common", configuration = "transformProductionFabric")) { isTransitive = false } +} + +java { + withSourcesJar() +} + +tasks { + processResources { + val modId: String by project + val modName: String by project + val modDescription: String by project + val githubProject: String by project + + inputs.property("id", modId) + inputs.property("group", project.group) + inputs.property("name", modName) + inputs.property("description", modDescription) + inputs.property("version", project.version) + inputs.property("github", githubProject) + + filesMatching("fabric.mod.json") { + expand( + "id" to modId, + "group" to project.group, + "name" to modName, + "description" to modDescription, + "version" to project.version, + "github" to githubProject, + ) + } + } + + shadowJar { + exclude("architectury.common.json") + + configurations = listOf(shadowCommon.get()) + archiveClassifier.set("dev-shadow") + } + + remapJar { + injectAccessWidener.set(true) + inputFile.set(shadowJar.get().archiveFile) + dependsOn(shadowJar) + archiveClassifier.set(null as String?) + + from(rootProject.file("LICENSE")) + } + + named("sourcesJar") { + archiveClassifier.set("dev-sources") + val commonSources = project(":common").tasks.named("sourcesJar") + dependsOn(commonSources) + from(commonSources.get().archiveFile.map { zipTree(it) }) + } + + remapSourcesJar { + archiveClassifier.set("sources") + } + + jar { + archiveClassifier.set("dev") + } +} + +components["java"].withGroovyBuilder { + "withVariantsFromConfiguration"(configurations["shadowRuntimeElements"]) { + "skip"() + } +} + +val changelogText: String by ext + +unifiedPublishing { + project { + displayName.set("${project.version} (Fabric)") + releaseType.set("release") + gameVersions.set(listOf("1.19.3", "1.19.4")) + gameLoaders.set(listOf("fabric", "quilt")) + changelog.set(changelogText) + + mainPublication(tasks.remapJar.get()) + secondaryPublication(tasks.remapSourcesJar.get().archiveFile) + + val modrinthId: String? by rootProject + if (modrinthId?.isNotEmpty() == true) { + modrinth { + token.set(findProperty("modrinth.token")?.toString() ?: "Modrinth publishing token not found") + id.set(modrinthId) + version.set("${project.version}-fabric") + } + } + + val curseforgeId: String? by rootProject + if (curseforgeId?.isNotEmpty() == true) { + curseforge { + token.set(findProperty("curseforge.token")?.toString() ?: "Curseforge publishing token not found") + id.set(curseforgeId) + gameVersions.add("Java 17") + } + } + } +} +rootProject.tasks["releaseMod"].dependsOn(tasks["publishUnified"]) + +publishing { + publications { + create("fabric") { + groupId = "dev.isxander.yacl" + artifactId = "yet-another-config-lib-fabric" + + from(components["java"]) + } + } +} +tasks.findByPath("publishFabricPublicationToReleasesRepository")?.let { + rootProject.tasks["releaseMod"].dependsOn(it) +} diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..6b6f6e4 --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "id": "${id}", + "provides": [ + "yet-another-config-lib" + ], + "version": "${version}", + "name": "${name}", + "description": "${description}", + "authors": [ + "isXander" + ], + "contact": { + "homepage": "https://isxander.dev", + "issues": "https://github.com/${github}/issues", + "sources": "https://github.com/${github}" + }, + "icon": "yacl-128x.png", + "license": "LGPL-3.0-or-later", + "environment": "*", + "depends": { + "fabricloader": ">=0.14.0", + "minecraft": "~1.19.4", + "java": ">=17", + "fabric-resource-loader-v0": "*" + }, + "mixins": [ + "yacl.mixins.json" + ], + "custom": { + "modmenu": { + "badges": ["library"] + } + } +} diff --git a/forge/.gitignore b/forge/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/forge/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/forge/build.gradle.kts b/forge/build.gradle.kts new file mode 100644 index 0000000..5ba9450 --- /dev/null +++ b/forge/build.gradle.kts @@ -0,0 +1,164 @@ +import org.gradle.jvm.tasks.Jar + +plugins { + alias(libs.plugins.architectury.loom) + alias(libs.plugins.shadow) + alias(libs.plugins.unified.publishing) +} + +architectury { + platformSetupLoomIde() + forge() +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(project(":common").loom.accessWidenerPath) + + forge { + mixinConfig("yacl.mixins.json") + + convertAccessWideners.set(true) + extraAccessWideners.add(loom.accessWidenerPath.get().asFile.name) + } +} + +val common by configurations.registering +val shadowCommon by configurations.registering +configurations.compileClasspath.get().extendsFrom(common.get()) +configurations["developmentForge"].extendsFrom(common.get()) + +val minecraftVersion: String = libs.versions.minecraft.get() + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + forge(libs.forge) + + "common"(project(path = ":common", configuration = "namedElements")) { isTransitive = false } + "shadowCommon"(project(path = ":common", configuration = "transformProductionForge")) { isTransitive = false } +} + +java { + withSourcesJar() +} + +tasks { + processResources { + val modId: String by rootProject + val modName: String by rootProject + val modDescription: String by rootProject + val githubProject: String by rootProject + val majorForge = libs.versions.forge.get().substringAfter('-').split('.').first() + + inputs.property("id", modId) + inputs.property("group", project.group) + inputs.property("name", modName) + inputs.property("description", modDescription) + inputs.property("version", project.version) + inputs.property("github", githubProject) + inputs.property("major_forge", majorForge) + + filesMatching(listOf("META-INF/mods.toml", "pack.mcmeta")) { + expand( + "id" to modId, + "group" to project.group, + "name" to modName, + "description" to modDescription, + "version" to project.version, + "github" to githubProject, + "major_forge" to majorForge, + ) + } + } + + shadowJar { + exclude("fabric.mod.json") + exclude("architectury.common.json") + + configurations = listOf(shadowCommon.get()) + archiveClassifier.set("dev-shadow") + } + + remapJar { + injectAccessWidener.set(true) + inputFile.set(shadowJar.get().archiveFile) + dependsOn(shadowJar) + archiveClassifier.set(null as String?) + + from(rootProject.file("LICENSE")) + } + + named("sourcesJar") { + archiveClassifier.set("dev-sources") + val commonSources = project(":common").tasks.named("sourcesJar") + dependsOn(commonSources) + from(commonSources.get().archiveFile.map { zipTree(it) }) + } + + remapSourcesJar { + archiveClassifier.set("sources") + } + + jar { + archiveClassifier.set("dev") + } +} + +components["java"].withGroovyBuilder { + "withVariantsFromConfiguration"(configurations["shadowRuntimeElements"]) { + "skip"() + } +} + +val changelogText: String by ext + +unifiedPublishing { + project { + displayName.set("${project.version} (Forge)") + releaseType.set("release") + gameVersions.set(listOf("1.19.3", "1.19.4")) + gameLoaders.set(listOf("forge")) + changelog.set(changelogText) + + mainPublication(tasks.remapJar.get()) + secondaryPublication(tasks.remapSourcesJar.get().archiveFile) + + val modrinthId: String? by rootProject + if (modrinthId?.isNotEmpty() == true) { + modrinth { + token.set(findProperty("modrinth.token")?.toString() ?: "Modrinth publishing token not found") + id.set(modrinthId) + version.set("${project.version}-forge") + } + } + + val curseforgeId: String? by rootProject + if (curseforgeId?.isNotEmpty() == true) { + curseforge { + token.set(findProperty("curseforge.token")?.toString() ?: "Curseforge publishing token not found") + id.set(curseforgeId) + gameVersions.add("Java 17") + } + } + } +} +rootProject.tasks["releaseMod"].dependsOn(tasks["publishUnified"]) + +publishing { + publications { + create("forge") { + groupId = "dev.isxander.yacl" + artifactId = "yet-another-config-lib-forge" + + from(components["java"]) + } + } +} +tasks.findByPath("publishForgePublicationToReleasesRepository")?.let { + rootProject.tasks["releaseMod"].dependsOn(it) +} diff --git a/forge/gradle.properties b/forge/gradle.properties new file mode 100644 index 0000000..32f842a --- /dev/null +++ b/forge/gradle.properties @@ -0,0 +1 @@ +loom.platform=forge \ No newline at end of file diff --git a/forge/src/main/java/dev/isxander/yacl/forge/YACLForgeEntrypoint.java b/forge/src/main/java/dev/isxander/yacl/forge/YACLForgeEntrypoint.java new file mode 100644 index 0000000..3360184 --- /dev/null +++ b/forge/src/main/java/dev/isxander/yacl/forge/YACLForgeEntrypoint.java @@ -0,0 +1,11 @@ +package dev.isxander.yacl.forge; + +import net.minecraftforge.fml.common.Mod; + +@Mod("yet_another_config_lib") +public class YACLForgeEntrypoint { + public YACLForgeEntrypoint() { + + } + +} diff --git a/forge/src/main/resources/META-INF/mods.toml b/forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..e6a0a07 --- /dev/null +++ b/forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,28 @@ +modLoader = "javafml" +loaderVersion = "[${major_forge},)" +#issueTrackerURL = "" +license = "LGPL-3.0-or-later" + +[[mods]] +modId = "${id}" +version = "${version}" +displayName = "${name}" +authors = "isXander" +description = ''' +${description} +''' +logoFile = "yacl-128x.png" + +[["dependencies.${id}"]] +modId = "forge" +mandatory = true +versionRange = "[${major_forge},)" +ordering = "NONE" +side = "BOTH" + +[["dependencies.${id}"]] +modId = "minecraft" +mandatory = true +versionRange = "[1.19.4,)" +ordering = "NONE" +side = "BOTH" diff --git a/forge/src/main/resources/pack.mcmeta b/forge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..a9c6340 --- /dev/null +++ b/forge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "${name}", + "pack_format": 14 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 866cd28..a89ff66 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,9 @@ -org.gradle.jvmargs=-Xmx3G +org.gradle.jvmargs=-Xmx4G -minecraftVersion=1.19.4 -fabricLoaderVersion=0.14.17 -qmBuild=1 - -modId=yet-another-config-lib +modId=yet_another_config_lib modName=YetAnotherConfigLib modDescription=YetAnotherConfigLib (yacl) is just that. A builder-based configuration library for Minecraft. -curseforgeId=667299 modrinthId=yacl -githubProject=isXander/YetAnotherConfigLib +curseforgeId=yacl +githubProject=isXander/YetAnotherConfigLib \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..e811335 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,48 @@ +[versions] +# Plugins +architectury_loom = "1.1.+" +architectury_plugin = "3.4.+" +shadow = "7.1.+" +quilt_quiltflower = "1.8.+" +unified_publishing = "0.1.+" +github_release = "2.+" +machete = "2.+" +grgit = "5.0.+" + +minecraft = "1.19.4" +quilt_mappings = "10" +fabric_loader = "0.14.19" + +# Common Dependencies +mixin_extras = "0.2.0-beta.6" + +# Fabric-like Dependencies +fabric_api = "0.79.0+1.19.4" +mod_menu = "6.2.1" + +# Forge Dependencies +forge = "1.19.4-45.0.47" + +[libraries] +minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } +fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_loader" } + +# Common Dependencies +mixin_extras_common = { module = "com.github.llamalad7.mixinextras:mixinextras-common", version.ref = "mixin_extras" } + +# Fabric-like Dependencies +fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric_api" } +mod_menu = { module = "com.terraformersmc:modmenu", version.ref = "mod_menu" } + +# Forge Dependencies +forge = { module = "net.minecraftforge:forge", version.ref = "forge" } + +[plugins] +architectury_loom = { id = "dev.architectury.loom", version.ref = "architectury_loom" } +architectury_plugin = { id = "architectury-plugin", version.ref = "architectury_plugin" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } + +unified_publishing = { id = "me.shedaniel.unified-publishing", version.ref = "unified_publishing" } +github_release = { id = "com.github.breadmoirai.github-release", version.ref = "github_release" } +machete = { id = "io.github.p03w.machete", version.ref = "machete" } +grgit = { id = "org.ajoberstar.grgit", version.ref = "grgit" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..876a219 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sun Apr 23 19:05:18 BST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -205,12 +205,6 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%"=="" @echo off +@if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. +if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd +if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle.kts b/settings.gradle.kts index 0ea7354..51b9105 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,9 +3,23 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven("https://maven.fabricmc.net") + maven("https://maven.architectury.dev/") + maven("https://maven.minecraftforge.net/") maven("https://maven.quiltmc.org/repository/release") } } +dependencyResolutionManagement { + versionCatalogs { + create("libs") + } +} + rootProject.name = "YetAnotherConfigLib" +include("common") +include("fabric") +include("forge") +include("test-common") +include("test-fabric") +include("test-forge") diff --git a/src/client/java/dev/isxander/yacl/api/Binding.java b/src/client/java/dev/isxander/yacl/api/Binding.java deleted file mode 100644 index ba5a2c0..0000000 --- a/src/client/java/dev/isxander/yacl/api/Binding.java +++ /dev/null @@ -1,64 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.impl.GenericBindingImpl; -import dev.isxander.yacl.mixin.client.OptionInstanceAccessor; -import net.minecraft.client.OptionInstance; -import org.apache.commons.lang3.Validate; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Controls modifying the bound option. - * Provides the default value, a setter and a getter. - */ -public interface Binding { - void setValue(T value); - - T getValue(); - - T defaultValue(); - - /** - * Creates a generic binding. - * - * @param def default value of the option, used to reset - * @param getter should return the current value of the option - * @param setter should set the option to the supplied value - */ - static Binding generic(T def, Supplier getter, Consumer setter) { - Validate.notNull(def, "`def` must not be null"); - Validate.notNull(getter, "`getter` must not be null"); - Validate.notNull(setter, "`setter` must not be null"); - - return new GenericBindingImpl<>(def, getter, setter); - } - - /** - * Creates a {@link Binding} for Minecraft's {@link OptionInstance} - */ - static Binding minecraft(OptionInstance minecraftOption) { - Validate.notNull(minecraftOption, "`minecraftOption` must not be null"); - - return new GenericBindingImpl<>( - ((OptionInstanceAccessor) (Object) minecraftOption).getInitialValue(), - minecraftOption::get, - minecraftOption::set - ); - } - - /** - * Creates an immutable binding that has no default and cannot be modified. - * - * @param value the value for the binding - */ - static Binding immutable(T value) { - Validate.notNull(value, "`value` must not be null"); - - return new GenericBindingImpl<>( - value, - () -> value, - changed -> {} - ); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/ButtonOption.java b/src/client/java/dev/isxander/yacl/api/ButtonOption.java deleted file mode 100644 index 88e1c4b..0000000 --- a/src/client/java/dev/isxander/yacl/api/ButtonOption.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.impl.ButtonOptionImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; - -public interface ButtonOption extends Option> { - /** - * Action to be executed upon button press - */ - BiConsumer action(); - - static dev.isxander.yacl.api.ButtonOption.Builder createBuilder() { - return new ButtonOptionImpl.BuilderImpl(); - } - - interface Builder { - /** - * Sets the name to be used by the option. - * - * @see Option#name() - */ - dev.isxander.yacl.api.ButtonOption.Builder name(@NotNull Component name); - - /** - * Sets the tooltip to be used by the option. - * Can be invoked twice to append more lines. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltips text lines - merged with a new-line on {@link Option.Builder#build()}. - */ - dev.isxander.yacl.api.ButtonOption.Builder tooltip(@NotNull Component... tooltips); - - dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull BiConsumer action); - - /** - * Action to be executed upon button press - * - * @see ButtonOption#action() - */ - @Deprecated - dev.isxander.yacl.api.ButtonOption.Builder action(@NotNull Consumer action); - - /** - * Sets if the option can be configured - * - * @see Option#available() - */ - dev.isxander.yacl.api.ButtonOption.Builder available(boolean available); - - /** - * Sets the controller for the option. - * This is how you interact and change the options. - * - * @see dev.isxander.yacl.gui.controllers - */ - dev.isxander.yacl.api.ButtonOption.Builder controller(@NotNull Function>> control); - - ButtonOption build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/ConfigCategory.java b/src/client/java/dev/isxander/yacl/api/ConfigCategory.java deleted file mode 100644 index 7764479..0000000 --- a/src/client/java/dev/isxander/yacl/api/ConfigCategory.java +++ /dev/null @@ -1,94 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.impl.ConfigCategoryImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -/** - * Separates {@link Option}s or {@link OptionGroup}s into multiple distinct sections. - * Served to a user as a button in the left column, - * upon pressing, the options list is filled with options contained within this category. - */ -public interface ConfigCategory { - /** - * Name of category, displayed as a button on the left column. - */ - @NotNull Component name(); - - /** - * Gets every {@link OptionGroup} in this category. - */ - @NotNull ImmutableList groups(); - - /** - * Tooltip (or description) of the category. - * Rendered on hover. - */ - @NotNull Component tooltip(); - - /** - * Creates a builder to construct a {@link ConfigCategory} - */ - static Builder createBuilder() { - return new ConfigCategoryImpl.BuilderImpl(); - } - - interface Builder extends OptionAddable { - /** - * Sets name of the category - * - * @see ConfigCategory#name() - */ - Builder name(@NotNull Component name); - - /** - * Adds an option to the root group of the category. - * To add to another group, use {@link Builder#group(OptionGroup)}. - * To construct an option, use {@link Option#createBuilder(Class)} - * - * @see ConfigCategory#groups() - * @see OptionGroup#isRoot() - */ - @Override - Builder option(@NotNull Option option); - - /** - * Adds multiple options to the root group of the category. - * To add to another group, use {@link Builder#groups(Collection)}. - * To construct an option, use {@link Option#createBuilder(Class)} - * - * @see ConfigCategory#groups() - * @see OptionGroup#isRoot() - */ - @Override - Builder options(@NotNull Collection> options); - - /** - * Adds an option group. - * To add an option to the root group, use {@link Builder#option(Option)} - * To construct a group, use {@link OptionGroup#createBuilder()} - */ - Builder group(@NotNull OptionGroup group); - - /** - * Adds multiple option groups. - * To add multiple options to the root group, use {@link Builder#options(Collection)} - * To construct a group, use {@link OptionGroup#createBuilder()} - */ - Builder groups(@NotNull Collection groups); - - /** - * Sets the tooltip to be used by the category. - * Can be invoked twice to append more lines. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. - */ - Builder tooltip(@NotNull Component... tooltips); - - ConfigCategory build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/Controller.java b/src/client/java/dev/isxander/yacl/api/Controller.java deleted file mode 100644 index 0b8e2ed..0000000 --- a/src/client/java/dev/isxander/yacl/api/Controller.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.network.chat.Component; - -/** - * Provides a widget to control the option. - */ -public interface Controller { - /** - * Gets the dedicated {@link Option} for this controller - */ - Option option(); - - /** - * Gets the formatted value based on {@link Option#pendingValue()} - */ - Component formatValue(); - - /** - * Provides a widget to display - * - * @param screen parent screen - */ - AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension); -} diff --git a/src/client/java/dev/isxander/yacl/api/LabelOption.java b/src/client/java/dev/isxander/yacl/api/LabelOption.java deleted file mode 100644 index 05c7214..0000000 --- a/src/client/java/dev/isxander/yacl/api/LabelOption.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.impl.LabelOptionImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -/** - * A label option is an easier way of creating a label with a {@link dev.isxander.yacl.gui.controllers.LabelController}. - * This option is immutable and cannot be disabled. Tooltips are supported through - * {@link Component} styling. - */ -public interface LabelOption extends Option { - @NotNull Component label(); - - /** - * Creates a new label option with the given label, skipping a builder for ease. - */ - static LabelOption create(@NotNull Component label) { - return new LabelOptionImpl(label); - } - - static dev.isxander.yacl.api.LabelOption.Builder createBuilder() { - return new LabelOptionImpl.BuilderImpl(); - } - - interface Builder { - /** - * Appends a line to the label - */ - dev.isxander.yacl.api.LabelOption.Builder line(@NotNull Component line); - - /** - * Appends multiple lines to the label - */ - dev.isxander.yacl.api.LabelOption.Builder lines(@NotNull Collection lines); - - LabelOption build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/ListOption.java b/src/client/java/dev/isxander/yacl/api/ListOption.java deleted file mode 100644 index afba8ee..0000000 --- a/src/client/java/dev/isxander/yacl/api/ListOption.java +++ /dev/null @@ -1,152 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.impl.ListOptionImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -/** - * A list option that takes form as an option group for UX. - * You add this option through {@link ConfigCategory.Builder#group(OptionGroup)}. Do NOT add as an option. - * Users can add remove and reshuffle a list type. You can use any controller you wish, there are no dedicated - * controllers for list types. List options do not manipulate your list but get and set the list with a - * regular binding for simplicity. - * - * You may apply option flags like a normal option and collapse like a normal group, it is a merge of them both. - * Methods in this interface marked with {@link ApiStatus.Internal} should not be used, and could be subject to - * change at any time - * @param - */ -public interface ListOption extends OptionGroup, Option> { - @Override - @NotNull ImmutableList> options(); - - /** - * Class of the entry type - */ - @NotNull Class elementTypeClass(); - - @ApiStatus.Internal - ListOptionEntry insertNewEntryToTop(); - - @ApiStatus.Internal - void insertEntry(int index, ListOptionEntry entry); - - @ApiStatus.Internal - int indexOf(ListOptionEntry entry); - - @ApiStatus.Internal - void removeEntry(ListOptionEntry entry); - - @ApiStatus.Internal - void addRefreshListener(Runnable changedListener); - - static Builder createBuilder(Class typeClass) { - return new ListOptionImpl.BuilderImpl<>(typeClass); - } - - interface Builder { - /** - * Sets name of the list, for UX purposes, a name should always be given, - * but isn't enforced. - * - * @see ListOption#name() - */ - Builder name(@NotNull Component name); - - /** - * Sets the tooltip to be used by the list. It is displayed like a normal - * group when you hover over the name. Entries do not allow a tooltip. - *

- * Can be invoked twice to append more lines. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltips text lines - merged with a new-line on {@link dev.isxander.yacl.api.ListOption.Builder#build()}. - */ - Builder tooltip(@NotNull Component... tooltips); - - /** - * Sets the value that is used when creating new entries - */ - Builder initial(@NotNull T initialValue); - - /** - * Sets the controller for the option. - * This is how you interact and change the options. - * - * @see dev.isxander.yacl.gui.controllers - */ - Builder controller(@NotNull Function, Controller> control); - - /** - * Sets the binding for the option. - * Used for default, getter and setter. - * - * @see Binding - */ - Builder binding(@NotNull Binding> binding); - - /** - * Sets the binding for the option. - * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} - * - * @param def default value of the option, used to reset - * @param getter should return the current value of the option - * @param setter should set the option to the supplied value - * @see Binding - */ - Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter); - - /** - * Sets if the option can be configured - * - * @see Option#available() - */ - Builder available(boolean available); - - /** - * Adds a flag to the option. - * Upon applying changes, all flags are executed. - * {@link Option#flags()} - */ - Builder flag(@NotNull OptionFlag... flag); - - /** - * Adds a flag to the option. - * Upon applying changes, all flags are executed. - * {@link Option#flags()} - */ - Builder flags(@NotNull Collection flags); - - /** - * Dictates if the group should be collapsed by default. - * If not set, it will not be collapsed by default. - * - * @see OptionGroup#collapsed() - */ - Builder collapsed(boolean collapsible); - - /** - * Adds a listener to the option. Invoked upon changing any of the list's entries. - * - * @see Option#addListener(BiConsumer) - */ - ListOption.Builder listener(@NotNull BiConsumer>, List> listener); - - /** - * Adds multiple listeners to the option. Invoked upon changing of any of the list's entries. - * - * @see Option#addListener(BiConsumer) - */ - ListOption.Builder listeners(@NotNull Collection>, List>> listeners); - - ListOption build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java b/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java deleted file mode 100644 index e0a3424..0000000 --- a/src/client/java/dev/isxander/yacl/api/ListOptionEntry.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableSet; -import org.jetbrains.annotations.NotNull; - -public interface ListOptionEntry extends Option { - ListOption parentGroup(); - - @Override - default @NotNull Class typeClass() { - return parentGroup().elementTypeClass(); - } - - @Override - default @NotNull ImmutableSet flags() { - return parentGroup().flags(); - } - - @Override - default boolean available() { - return parentGroup().available(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/NameableEnum.java b/src/client/java/dev/isxander/yacl/api/NameableEnum.java deleted file mode 100644 index 4b04057..0000000 --- a/src/client/java/dev/isxander/yacl/api/NameableEnum.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.isxander.yacl.api; - -import net.minecraft.network.chat.Component; - -/** - * Used for the default value formatter of {@link dev.isxander.yacl.gui.controllers.cycling.EnumController} - */ -public interface NameableEnum { - Component getDisplayName(); -} diff --git a/src/client/java/dev/isxander/yacl/api/Option.java b/src/client/java/dev/isxander/yacl/api/Option.java deleted file mode 100644 index a6c0311..0000000 --- a/src/client/java/dev/isxander/yacl/api/Option.java +++ /dev/null @@ -1,227 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.impl.OptionImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -public interface Option { - /** - * Name of the option - */ - @NotNull Component name(); - - /** - * Tooltip (or description) of the option. - * Rendered on hover. - */ - @NotNull Component tooltip(); - - /** - * Widget provider for a type of option. - * - * @see dev.isxander.yacl.gui.controllers - */ - @NotNull Controller controller(); - - /** - * Binding for the option. - * Controls setting, getting and default value. - * - * @see Binding - */ - @NotNull Binding binding(); - - /** - * If the option can be configured - */ - boolean available(); - - /** - * Sets if the option can be configured after being built - * - * @see Option#available() - */ - void setAvailable(boolean available); - - /** - * Class of the option type. - * Used by some controllers. - */ - @NotNull Class typeClass(); - - /** - * Tasks that needs to be executed upon applying changes. - */ - @NotNull ImmutableSet flags(); - - /** - * Checks if the pending value is not equal to the current set value - */ - boolean changed(); - - /** - * Value in the GUI, ready to set the actual bound value or be undone. - */ - @NotNull T pendingValue(); - - /** - * Sets the pending value - */ - void requestSet(T value); - - /** - * Applies the pending value to the bound value. - * Cannot be undone. - * - * @return if there were changes to apply {@link Option#changed()} - */ - boolean applyValue(); - - /** - * Sets the pending value to the bound value. - */ - void forgetPendingValue(); - - /** - * Sets the pending value to the default bound value. - */ - void requestSetDefault(); - - /** - * Checks if the current pending value is equal to its default value - */ - boolean isPendingValueDefault(); - - default boolean canResetToDefault() { - return true; - } - - /** - * Adds a listener for when the pending value changes - */ - void addListener(BiConsumer, T> changedListener); - - /** - * Creates a builder to construct an {@link Option} - * - * @param type of the option's value - * @param typeClass used to capture the type - */ - static Builder createBuilder(Class typeClass) { - return new OptionImpl.BuilderImpl<>(typeClass); - } - - interface Builder { - /** - * Sets the name to be used by the option. - * - * @see Option#name() - */ - Builder name(@NotNull Component name); - - /** - * Sets the tooltip to be used by the option. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. - */ - Builder tooltip(@NotNull Function tooltipGetter); - - /** - * Sets the tooltip to be used by the option. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}. - */ - @Deprecated - Builder tooltip(@NotNull Function... tooltipGetter); - - /** - * Sets the tooltip to be used by the option. - * Can be invoked twice to append more lines. - * No need to wrap the text yourself, the gui does this itself. - * - * @param tooltips text lines - merged with a new-line on {@link Builder#build()}. - */ - Builder tooltip(@NotNull Component... tooltips); - - /** - * Sets the controller for the option. - * This is how you interact and change the options. - * - * @see dev.isxander.yacl.gui.controllers - */ - Builder controller(@NotNull Function, Controller> control); - - /** - * Sets the binding for the option. - * Used for default, getter and setter. - * - * @see Binding - */ - Builder binding(@NotNull Binding binding); - - /** - * Sets the binding for the option. - * Shorthand of {@link Binding#generic(Object, Supplier, Consumer)} - * - * @param def default value of the option, used to reset - * @param getter should return the current value of the option - * @param setter should set the option to the supplied value - * @see Binding - */ - Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter); - - /** - * Sets if the option can be configured - * - * @see Option#available() - */ - Builder available(boolean available); - - /** - * Adds a flag to the option. - * Upon applying changes, all flags are executed. - * {@link Option#flags()} - */ - Builder flag(@NotNull OptionFlag... flag); - - /** - * Adds a flag to the option. - * Upon applying changes, all flags are executed. - * {@link Option#flags()} - */ - Builder flags(@NotNull Collection flags); - - /** - * Instantly invokes the binder's setter when modified in the GUI. - * Prevents the user from undoing the change - *

- * Does not support {@link Option#flags()}! - */ - Builder instant(boolean instant); - - /** - * Adds a listener to the option. Invoked upon changing the pending value. - * - * @see Option#addListener(BiConsumer) - */ - Builder listener(@NotNull BiConsumer, T> listener); - - /** - * Adds multiple listeners to the option. Invoked upon changing the pending value. - * - * @see Option#addListener(BiConsumer) - */ - Builder listeners(@NotNull Collection, T>> listeners); - - Option build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/OptionAddable.java b/src/client/java/dev/isxander/yacl/api/OptionAddable.java deleted file mode 100644 index 57be06c..0000000 --- a/src/client/java/dev/isxander/yacl/api/OptionAddable.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.isxander.yacl.api; - -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -public interface OptionAddable { - /** - * Adds an option to an abstract builder. - * To construct an option, use {@link Option#createBuilder(Class)} - */ - OptionAddable option(@NotNull Option option); - - /** - * Adds multiple options to an abstract builder. - * To construct an option, use {@link Option#createBuilder(Class)} - */ - OptionAddable options(@NotNull Collection> options); -} diff --git a/src/client/java/dev/isxander/yacl/api/OptionFlag.java b/src/client/java/dev/isxander/yacl/api/OptionFlag.java deleted file mode 100644 index 51d57e4..0000000 --- a/src/client/java/dev/isxander/yacl/api/OptionFlag.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.gui.RequireRestartScreen; -import net.minecraft.client.Minecraft; - -import java.util.function.Consumer; - -/** - * Code that is executed upon certain options being applied. - * Each flag is executed only once per save, no matter the amount of options with the flag. - */ -@FunctionalInterface -public interface OptionFlag extends Consumer { - /** Warns the user that a game restart is required for the changes to take effect */ - OptionFlag GAME_RESTART = client -> client.setScreen(new RequireRestartScreen(client.screen)); - - /** Reloads chunks upon applying (F3+A) */ - OptionFlag RELOAD_CHUNKS = client -> client.levelRenderer.allChanged(); - - OptionFlag WORLD_RENDER_UPDATE = client -> client.levelRenderer.needsUpdate(); - - OptionFlag ASSET_RELOAD = Minecraft::delayTextureReload; -} diff --git a/src/client/java/dev/isxander/yacl/api/OptionGroup.java b/src/client/java/dev/isxander/yacl/api/OptionGroup.java deleted file mode 100644 index 4fe43c7..0000000 --- a/src/client/java/dev/isxander/yacl/api/OptionGroup.java +++ /dev/null @@ -1,94 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.impl.OptionGroupImpl; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.Collection; - -/** - * Serves as a separator between multiple chunks of options - * that may be too similar or too few to be placed in a separate {@link ConfigCategory}. - * Or maybe you just want your config to feel less dense. - */ -public interface OptionGroup { - /** - * Name of the option group, displayed as a separator in the option lists. - * Can be empty. - */ - Component name(); - - /** - * Tooltip displayed on hover. - */ - Component tooltip(); - - /** - * List of all options in the group - */ - @NotNull ImmutableList> options(); - - /** - * Dictates if the group should be collapsed by default. - */ - boolean collapsed(); - - /** - * Always false when using the {@link Builder} - * used to not render the separator if true - */ - boolean isRoot(); - - /** - * Creates a builder to construct a {@link OptionGroup} - */ - static Builder createBuilder() { - return new OptionGroupImpl.BuilderImpl(); - } - - interface Builder extends OptionAddable { - /** - * Sets name of the group, can be {@link Component#empty()} to just separate options, like sodium. - * - * @see OptionGroup#name() - */ - Builder name(@NotNull Component name); - - /** - * Sets the tooltip to be used by the option group. - * Can be invoked twice to append more lines. - * No need to wrap the Component yourself, the gui does this itself. - * - * @param tooltips Component lines - merged with a new-line on {@link Builder#build()}. - */ - Builder tooltip(@NotNull Component... tooltips); - - /** - * Adds an option to group. - * To construct an option, use {@link Option#createBuilder(Class)} - * - * @see OptionGroup#options() - */ - @Override - Builder option(@NotNull Option option); - - /** - * Adds multiple options to group. - * To construct an option, use {@link Option#createBuilder(Class)} - * - * @see OptionGroup#options() - */ - @Override - Builder options(@NotNull Collection> options); - - /** - * Dictates if the group should be collapsed by default - * - * @see OptionGroup#collapsed() - */ - Builder collapsed(boolean collapsible); - - OptionGroup build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java b/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java deleted file mode 100644 index 3641fad..0000000 --- a/src/client/java/dev/isxander/yacl/api/PlaceholderCategory.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.isxander.yacl.api; - -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.impl.PlaceholderCategoryImpl; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; - -import java.util.function.BiFunction; - -/** - * A placeholder category that actually just opens another screen, - * instead of displaying options - */ -public interface PlaceholderCategory extends ConfigCategory { - /** - * Function to create a screen to open upon changing to this category - */ - BiFunction screen(); - - static Builder createBuilder() { - return new PlaceholderCategoryImpl.BuilderImpl(); - } - - interface Builder { - /** - * Sets name of the category - * - * @see ConfigCategory#name() - */ - Builder name(@NotNull Component name); - - /** - * Sets the tooltip to be used by the category. - * Can be invoked twice to append more lines. - * No need to wrap the Component yourself, the gui does this itself. - * - * @param tooltips Component lines - merged with a new-line on {@link dev.isxander.yacl.api.PlaceholderCategory.Builder#build()}. - */ - Builder tooltip(@NotNull Component... tooltips); - - /** - * Screen to open upon selecting this category - * - * @see PlaceholderCategory#screen() - */ - Builder screen(@NotNull BiFunction screenFunction); - - PlaceholderCategory build(); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java b/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java deleted file mode 100644 index c6da1d1..0000000 --- a/src/client/java/dev/isxander/yacl/api/YetAnotherConfigLib.java +++ /dev/null @@ -1,107 +0,0 @@ -package dev.isxander.yacl.api; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.config.ConfigInstance; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.impl.YetAnotherConfigLibImpl; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Collection; -import java.util.function.Consumer; - -/** - * Main class of the mod. - * Contains all data and used to provide a {@link Screen} - */ -public interface YetAnotherConfigLib { - /** - * Title of the GUI. Only used for Minecraft narration. - */ - Component title(); - - /** - * Gets all config categories. - */ - ImmutableList categories(); - - /** - * Ran when changes are saved. Can be used to save config to a file etc. - */ - Runnable saveFunction(); - - /** - * Ran every time the YACL screen initialises. Can be paired with FAPI to add custom widgets. - */ - Consumer initConsumer(); - - /** - * Generates a Screen to display based on this instance. - * - * @param parent parent screen to open once closed - */ - Screen generateScreen(@Nullable Screen parent); - - /** - * Creates a builder to construct YACL - */ - static Builder createBuilder() { - return new YetAnotherConfigLibImpl.BuilderImpl(); - } - - /** - * Creates an instance using a {@link ConfigInstance} which autofills the save() builder method. - * This also takes an easy functional interface that provides defaults and config to help build YACL bindings. - */ - static YetAnotherConfigLib create(ConfigInstance configInstance, ConfigBackedBuilder builder) { - return builder.build(configInstance.getDefaults(), configInstance.getConfig(), createBuilder().save(configInstance::save)).build(); - } - - interface Builder { - /** - * Sets title of GUI for Minecraft narration - * - * @see YetAnotherConfigLib#title() - */ - Builder title(@NotNull Component title); - - /** - * Adds a new category. - * To create a category you need to use {@link ConfigCategory#createBuilder()} - * - * @see YetAnotherConfigLib#categories() - */ - Builder category(@NotNull ConfigCategory category); - - /** - * Adds multiple categories at once. - * To create a category you need to use {@link ConfigCategory#createBuilder()} - * - * @see YetAnotherConfigLib#categories() - */ - Builder categories(@NotNull Collection categories); - - /** - * Used to define a save function for when user clicks the Save Changes button - * - * @see YetAnotherConfigLib#saveFunction() - */ - Builder save(@NotNull Runnable saveFunction); - - /** - * Defines a consumer that is accepted every time the YACL screen initialises - * - * @see YetAnotherConfigLib#initConsumer() - */ - Builder screenInit(@NotNull Consumer initConsumer); - - YetAnotherConfigLib build(); - } - - @FunctionalInterface - interface ConfigBackedBuilder { - YetAnotherConfigLib.Builder build(T defaults, T config, YetAnotherConfigLib.Builder builder); - } -} diff --git a/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java b/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java deleted file mode 100644 index 22032bd..0000000 --- a/src/client/java/dev/isxander/yacl/api/utils/OptionUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.isxander.yacl.api.utils; - -import dev.isxander.yacl.api.*; - -import java.util.function.Consumer; -import java.util.function.Function; - -public class OptionUtils { - /** - * Consumes all options, ignoring groups and categories. - * When consumer returns true, this function stops iterating. - */ - public static void consumeOptions(YetAnotherConfigLib yacl, Function, Boolean> consumer) { - for (ConfigCategory category : yacl.categories()) { - for (OptionGroup group : category.groups()) { - if (group instanceof ListOption list) { - if (consumer.apply(list)) return; - } else { - for (Option option : group.options()) { - if (consumer.apply(option)) return; - } - } - - } - } - } - - /** - * Consumes all options, ignoring groups and categories. - * - * @see OptionUtils#consumeOptions(YetAnotherConfigLib, Function) - */ - public static void forEachOptions(YetAnotherConfigLib yacl, Consumer> consumer) { - consumeOptions(yacl, (opt) -> { - consumer.accept(opt); - return false; - }); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java b/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java deleted file mode 100644 index c6d2b09..0000000 --- a/src/client/java/dev/isxander/yacl/gui/AbstractWidget.java +++ /dev/null @@ -1,107 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.utils.Dimension; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.components.Renderable; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.client.resources.sounds.SimpleSoundInstance; -import net.minecraft.sounds.SoundEvents; - -import java.awt.Color; - -public abstract class AbstractWidget implements GuiEventListener, Renderable, NarratableEntry { - protected final Minecraft client = Minecraft.getInstance(); - protected final Font textRenderer = client.font; - protected final int inactiveColor = 0xFFA0A0A0; - - private Dimension dim; - - public AbstractWidget(Dimension dim) { - this.dim = dim; - } - - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - - } - - public boolean canReset() { - return false; - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - if (dim == null) return false; - return this.dim.isPointInside((int) mouseX, (int) mouseY); - } - - public void setDimension(Dimension dim) { - this.dim = dim; - } - - public Dimension getDimension() { - return dim; - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.NONE; - } - - public void unfocus() { - - } - - public boolean matchesSearch(String query) { - return true; - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - - } - - protected void drawButtonRect(PoseStack matrices, int x1, int y1, int x2, int y2, boolean hovered, boolean enabled) { - if (x1 > x2) { - int xx1 = x1; - x1 = x2; - x2 = xx1; - } - if (y1 > y2) { - int yy1 = y1; - y1 = y2; - y2 = yy1; - } - int width = x2 - x1; - int height = y2 - y1; - - RenderSystem.setShader(GameRenderer::getPositionTexShader); - RenderSystem.setShaderTexture(0, net.minecraft.client.gui.components.AbstractWidget.WIDGETS_LOCATION); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - int i = !enabled ? 0 : hovered ? 2 : 1; - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - RenderSystem.enableDepthTest(); - GuiComponent.blit(matrices, x1, y1, 0, 0, 46 + i * 20, width / 2, height, 256, 256); - GuiComponent.blit(matrices, x1 + width / 2, y1, 0, 200 - width / 2f, 46 + i * 20, width / 2, height, 256, 256); - } - - protected int multiplyColor(int hex, float amount) { - Color color = new Color(hex, true); - - return new Color(Math.max((int)(color.getRed() * amount), 0), - Math.max((int)(color.getGreen() * amount), 0), - Math.max((int)(color.getBlue() * amount), 0), - color.getAlpha()).getRGB(); - } - - public void playDownSound() { - Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java b/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java deleted file mode 100644 index 41286ff..0000000 --- a/src/client/java/dev/isxander/yacl/gui/CategoryListWidget.java +++ /dev/null @@ -1,99 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.ConfigCategory; -import dev.isxander.yacl.gui.utils.GuiUtils; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; - -import java.util.List; - -public class CategoryListWidget extends ElementListWidgetExt { - private final YACLScreen yaclScreen; - - public CategoryListWidget(Minecraft client, YACLScreen yaclScreen, int screenWidth, int screenHeight) { - super(client, 0, 0, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, true); - this.yaclScreen = yaclScreen; - setRenderBackground(false); - setRenderTopAndBottom(false); - - for (ConfigCategory category : yaclScreen.config.categories()) { - addEntry(new CategoryEntry(category)); - } - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - GuiUtils.enableScissor(0, 0, width, height); - super.render(matrices, mouseX, mouseY, delta); - RenderSystem.disableScissor(); - } - - @Override - public int getRowWidth() { - return Math.min(width - width / 10, 396); - } - - @Override - public int getRowLeft() { - return super.getRowLeft() - 2; - } - - @Override - protected int getScrollbarPosition() { - return width - 2; - } - - @Override - protected void renderBackground(PoseStack matrices) { - - } - - public class CategoryEntry extends Entry { - private final CategoryWidget categoryButton; - public final int categoryIndex; - - public CategoryEntry(ConfigCategory category) { - this.categoryIndex = yaclScreen.config.categories().indexOf(category); - categoryButton = new CategoryWidget( - yaclScreen, - category, - categoryIndex, - getRowLeft(), 0, - getRowWidth(), 20 - ); - } - - @Override - public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - if (mouseY > y1) { - mouseY = -20; - } - - categoryButton.setY(y); - categoryButton.render(matrices, mouseX, mouseY, tickDelta); - } - - public void postRender(PoseStack matrices, int mouseX, int mouseY, float tickDelta) { - categoryButton.renderHoveredTooltip(matrices); - } - - @Override - public int getItemHeight() { - return 21; - } - - @Override - public List children() { - return ImmutableList.of(categoryButton); - } - - @Override - public List narratables() { - return ImmutableList.of(categoryButton); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java b/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java deleted file mode 100644 index 60817a2..0000000 --- a/src/client/java/dev/isxander/yacl/gui/CategoryWidget.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.isxander.yacl.gui; - -import dev.isxander.yacl.api.ConfigCategory; -import net.minecraft.client.sounds.SoundManager; - -public class CategoryWidget extends TooltipButtonWidget { - private final int categoryIndex; - - public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) { - super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> { - screen.searchFieldWidget.setValue(""); - screen.changeCategory(categoryIndex); - }); - this.categoryIndex = categoryIndex; - } - - private boolean isCurrentCategory() { - return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex; - } - - @Override - protected int getTextureY() { - int i = 1; - if (!this.active) { - i = 0; - } else if (this.isHoveredOrFocused() || isCurrentCategory()) { - i = 2; - } - - return 46 + i * 20; - } - - @Override - public void playDownSound(SoundManager soundManager) { - if (!isCurrentCategory()) - super.playDownSound(soundManager); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/ElementListWidgetExt.java b/src/client/java/dev/isxander/yacl/gui/ElementListWidgetExt.java deleted file mode 100644 index 46503a6..0000000 --- a/src/client/java/dev/isxander/yacl/gui/ElementListWidgetExt.java +++ /dev/null @@ -1,179 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.ContainerObjectSelectionList; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.util.Mth; -import org.jetbrains.annotations.Nullable; - -import java.util.Iterator; - -public class ElementListWidgetExt> extends ContainerObjectSelectionList { - protected final int x, y; - - private double smoothScrollAmount = getScrollAmount(); - private boolean returnSmoothAmount = false; - private final boolean doSmoothScrolling; - - public ElementListWidgetExt(Minecraft client, int x, int y, int width, int height, boolean smoothScrolling) { - super(client, width, height, y, y + height, 22); - this.x = this.x0 = x; - this.y = y; - this.x1 = this.x0 + width; - this.doSmoothScrolling = smoothScrolling; - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double amount) { - // default implementation bases scroll step from total height of entries, this is constant - this.setScrollAmount(this.getScrollAmount() - amount * 20); - return true; - } - - @Override - protected void renderBackground(PoseStack matrices) { - // render transparent background if in-game. - setRenderBackground(minecraft.level == null); - if (minecraft.level != null) - fill(matrices, x0, y0, x1, y1, 0x6B000000); - } - - @Override - protected int getScrollbarPosition() { - // default implementation does not respect left/right - return this.x1 - 2; - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount()); - returnSmoothAmount = true; - super.render(matrices, mouseX, mouseY, delta); - returnSmoothAmount = false; - } - - /** - * awful code to only use smooth scroll state when rendering, - * not other code that needs target scroll amount - */ - @Override - public double getScrollAmount() { - if (returnSmoothAmount && doSmoothScrolling) - return smoothScrollAmount; - - return super.getScrollAmount(); - } - - protected void resetSmoothScrolling() { - this.smoothScrollAmount = getScrollAmount(); - } - - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - for (E entry : children()) { - entry.postRender(matrices, mouseX, mouseY, delta); - } - } - - @Nullable - @Override - protected E getEntryAtPosition(double x, double y) { - y += getScrollAmount(); - - if (x < this.x0 || x > this.x1) - return null; - - int currentY = this.y0 - headerHeight + 4; - for (E entry : children()) { - if (y >= currentY && y <= currentY + entry.getItemHeight()) { - return entry; - } - - currentY += entry.getItemHeight(); - } - - return null; - } - - /* - below code is licensed from cloth-config under LGPL3 - modified to inherit vanilla's EntryListWidget and use yarn mappings - - code is responsible for having dynamic item heights - */ - - @Override - protected int getMaxPosition() { - return children().stream().map(E::getItemHeight).reduce(0, Integer::sum) + headerHeight; - } - - @Override - protected void centerScrollOn(E entry) { - double d = (this.height) / -2d; - for (int i = 0; i < this.children().indexOf(entry) && i < this.getItemCount(); i++) - d += children().get(i).getItemHeight(); - this.setScrollAmount(d); - } - - @Override - protected int getRowTop(int index) { - int integer = y0 + 4 - (int) this.getScrollAmount() + headerHeight; - for (int i = 0; i < children().size() && i < index; i++) - integer += children().get(i).getItemHeight(); - return integer; - } - - @Override - protected void renderList(PoseStack matrices, int mouseX, int mouseY, float delta) { - int left = this.getRowLeft(); - int right = this.getRowWidth(); - int count = this.getItemCount(); - - for(int i = 0; i < count; ++i) { - E entry = children().get(i); - int top = this.getRowTop(i); - int bottom = top + entry.getItemHeight(); - int entryHeight = entry.getItemHeight() - 4; - if (bottom >= this.y0 && top <= this.y1) { - this.renderItem(matrices, mouseX, mouseY, delta, i, left, top, right, entryHeight); - } - } - } - - /* END cloth config code */ - - public abstract static class Entry> extends ContainerObjectSelectionList.Entry { - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - for (GuiEventListener child : this.children()) { - if (child.mouseClicked(mouseX, mouseY, button)) { - if (button == InputConstants.MOUSE_BUTTON_LEFT) - this.setDragging(true); - return true; - } - } - - return false; - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - if (isDragging() && button == InputConstants.MOUSE_BUTTON_LEFT) { - for (GuiEventListener child : this.children()) { - if (child.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) - return true; - } - } - return false; - } - - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - - } - - public int getItemHeight() { - return 22; - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java deleted file mode 100644 index 7e17aac..0000000 --- a/src/client/java/dev/isxander/yacl/gui/LowProfileButtonWidget.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.Tooltip; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; - -public class LowProfileButtonWidget extends Button { - public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress) { - super(x, y, width, height, message, onPress, DEFAULT_NARRATION); - } - - public LowProfileButtonWidget(int x, int y, int width, int height, Component message, OnPress onPress, Tooltip tooltip) { - this(x, y, width, height, message, onPress); - setTooltip(tooltip); - } - - @Override - public void renderWidget(PoseStack matrices, int mouseX, int mouseY, float deltaTicks) { - if (!isHoveredOrFocused() || !active) { - int j = this.active ? 0xFFFFFF : 0xA0A0A0; - this.renderString(matrices, Minecraft.getInstance().font, j); - } else { - super.renderWidget(matrices, mouseX, mouseY, deltaTicks); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java b/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java deleted file mode 100644 index 8f96ddf..0000000 --- a/src/client/java/dev/isxander/yacl/gui/OptionListWidget.java +++ /dev/null @@ -1,568 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.impl.utils.YACLConstants; -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.narration.NarratedElementType; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -public class OptionListWidget extends ElementListWidgetExt { - private final YACLScreen yaclScreen; - private boolean singleCategory = false; - - private ImmutableList viewableChildren; - - public OptionListWidget(YACLScreen screen, Minecraft client, int width, int height) { - super(client, width / 3, 0, width / 3 * 2 + 1, height, true); - this.yaclScreen = screen; - - refreshOptions(); - - for (ConfigCategory category : screen.config.categories()) { - for (OptionGroup group : category.groups()) { - if (group instanceof ListOption listOption) { - listOption.addRefreshListener(() -> refreshListEntries(listOption, category)); - } - } - } - } - - public void refreshOptions() { - clearEntries(); - - List categories = new ArrayList<>(); - if (yaclScreen.getCurrentCategoryIdx() == -1) { - // -1 = no category, search in progress, so use all categories for search - categories.addAll(yaclScreen.config.categories()); - } else { - categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx())); - } - singleCategory = categories.size() == 1; - - for (ConfigCategory category : categories) { - for (OptionGroup group : category.groups()) { - GroupSeparatorEntry groupSeparatorEntry; - if (!group.isRoot()) { - groupSeparatorEntry = group instanceof ListOption listOption - ? new ListGroupSeparatorEntry(listOption, yaclScreen) - : new GroupSeparatorEntry(group, yaclScreen); - addEntry(groupSeparatorEntry); - } else { - groupSeparatorEntry = null; - } - - List optionEntries = new ArrayList<>(); - - // add empty entry to make sure users know it's empty not just bugging out - if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) { - if (listGroupSeparatorEntry.listOption.options().isEmpty()) { - EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category); - addEntry(emptyListLabel); - optionEntries.add(emptyListLabel); - } - } - - for (Option option : group.options()) { - OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); - addEntry(entry); - optionEntries.add(entry); - } - - if (groupSeparatorEntry != null) { - groupSeparatorEntry.setChildEntries(optionEntries); - } - } - } - - recacheViewableChildren(); - setScrollAmount(0); - resetSmoothScrolling(); - } - - private void refreshListEntries(ListOption listOption, ConfigCategory category) { - // find group separator for group - ListGroupSeparatorEntry groupSeparator = super.children().stream().filter(e -> e instanceof ListGroupSeparatorEntry gs && gs.group == listOption).map(ListGroupSeparatorEntry.class::cast).findAny().orElse(null); - - if (groupSeparator == null) { - YACLConstants.LOGGER.warn("Can't find group seperator to refresh list option entries for list option " + listOption.name()); - return; - } - - for (dev.isxander.yacl.gui.OptionListWidget.Entry entry : groupSeparator.childEntries) - super.removeEntry(entry); - groupSeparator.childEntries.clear(); - - // if no entries, below loop won't run where addEntryBelow() recaches viewable children - if (listOption.options().isEmpty()) { - EmptyListLabel emptyListLabel; - addEntryBelow(groupSeparator, emptyListLabel = new EmptyListLabel(groupSeparator, category)); - groupSeparator.childEntries.add(emptyListLabel); - return; - } - - dev.isxander.yacl.gui.OptionListWidget.Entry lastEntry = groupSeparator; - for (ListOptionEntry listOptionEntry : listOption.options()) { - OptionEntry optionEntry = new OptionEntry(listOptionEntry, category, listOption, groupSeparator, listOptionEntry.controller().provideWidget(yaclScreen, getDefaultEntryDimension())); - addEntryBelow(lastEntry, optionEntry); - groupSeparator.childEntries.add(optionEntry); - lastEntry = optionEntry; - } - } - - public Dimension getDefaultEntryDimension() { - return Dimension.ofInt(getRowLeft(), 0, getRowWidth(), 20); - } - - public void expandAllGroups() { - for (dev.isxander.yacl.gui.OptionListWidget.Entry entry : super.children()) { - if (entry instanceof GroupSeparatorEntry groupSeparatorEntry) { - groupSeparatorEntry.setExpanded(true); - } - } - } - - @Override - public int getRowWidth() { - return Math.min(396, (int)(width / 1.3f)); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - for (dev.isxander.yacl.gui.OptionListWidget.Entry child : children()) { - if (child != getEntryAtPosition(mouseX, mouseY) && child instanceof OptionEntry optionEntry) - optionEntry.widget.unfocus(); - } - - return super.mouseClicked(mouseX, mouseY, button); - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double amount) { - super.mouseScrolled(mouseX, mouseY, amount); - - for (dev.isxander.yacl.gui.OptionListWidget.Entry child : children()) { - if (child.mouseScrolled(mouseX, mouseY, amount)) - break; - } - - return true; - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - for (dev.isxander.yacl.gui.OptionListWidget.Entry child : children()) { - if (child.keyPressed(keyCode, scanCode, modifiers)) - return true; - } - - return super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean charTyped(char chr, int modifiers) { - for (dev.isxander.yacl.gui.OptionListWidget.Entry child : children()) { - if (child.charTyped(chr, modifiers)) - return true; - } - - return super.charTyped(chr, modifiers); - } - - @Override - protected int getScrollbarPosition() { - return x1 - (int)(width * 0.05f); - } - - public void recacheViewableChildren() { - this.viewableChildren = ImmutableList.copyOf(super.children().stream().filter(dev.isxander.yacl.gui.OptionListWidget.Entry::isViewable).toList()); - - // update y positions before they need to be rendered are rendered - int i = 0; - for (dev.isxander.yacl.gui.OptionListWidget.Entry entry : viewableChildren) { - if (entry instanceof OptionEntry optionEntry) - optionEntry.widget.setDimension(optionEntry.widget.getDimension().withY(getRowTop(i))); - i++; - } - } - - @Override - public List children() { - return viewableChildren; - } - - public void addEntry(int index, Entry entry) { - super.children().add(index, entry); - recacheViewableChildren(); - } - - public void addEntryBelow(Entry below, Entry entry) { - int idx = super.children().indexOf(below) + 1; - - if (idx == 0) - throw new IllegalStateException("The entry to insert below does not exist!"); - - addEntry(idx, entry); - } - - public void addEntryBelowWithoutScroll(Entry below, Entry entry) { - double d = (double)this.getMaxScroll() - this.getScrollAmount(); - addEntryBelow(below, entry); - setScrollAmount(getMaxScroll() - d); - } - - @Override - public boolean removeEntryFromTop(Entry entry) { - boolean ret = super.removeEntryFromTop(entry); - recacheViewableChildren(); - return ret; - } - - @Override - public boolean removeEntry(Entry entry) { - boolean ret = super.removeEntry(entry); - recacheViewableChildren(); - return ret; - } - - public abstract class Entry extends ElementListWidgetExt.Entry { - public boolean isViewable() { - return true; - } - - protected boolean isHovered() { - return Objects.equals(getHovered(), this); - } - } - - public class OptionEntry extends Entry { - public final Option option; - public final ConfigCategory category; - public final OptionGroup group; - - public final @Nullable GroupSeparatorEntry groupSeparatorEntry; - - public final AbstractWidget widget; - - private final TextScaledButtonWidget resetButton; - - private final String categoryName; - private final String groupName; - - public OptionEntry(Option option, ConfigCategory category, OptionGroup group, @Nullable GroupSeparatorEntry groupSeparatorEntry, AbstractWidget widget) { - this.option = option; - this.category = category; - this.group = group; - this.groupSeparatorEntry = groupSeparatorEntry; - this.widget = widget; - this.categoryName = category.name().getString().toLowerCase(); - this.groupName = group.name().getString().toLowerCase(); - if (option.canResetToDefault() && this.widget.canReset()) { - this.widget.setDimension(this.widget.getDimension().expanded(-20, 0)); - this.resetButton = new TextScaledButtonWidget(widget.getDimension().xLimit(), -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { - option.requestSetDefault(); - }); - option.addListener((opt, val) -> this.resetButton.active = !opt.isPendingValueDefault() && opt.available()); - this.resetButton.active = !option.isPendingValueDefault() && option.available(); - } else { - this.resetButton = null; - } - } - - @Override - public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - widget.setDimension(widget.getDimension().withY(y)); - - widget.render(matrices, mouseX, mouseY, tickDelta); - - if (resetButton != null) { - resetButton.setY(y); - resetButton.render(matrices, mouseX, mouseY, tickDelta); - } - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - widget.postRender(matrices, mouseX, mouseY, delta); - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double amount) { - return widget.mouseScrolled(mouseX, mouseY, amount); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - return widget.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean charTyped(char chr, int modifiers) { - return widget.charTyped(chr, modifiers); - } - - @Override - public boolean isViewable() { - String query = yaclScreen.searchFieldWidget.getQuery(); - return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded()) - && (yaclScreen.searchFieldWidget.isEmpty() - || (!singleCategory && categoryName.contains(query)) - || groupName.contains(query) - || widget.matchesSearch(query)); - } - - @Override - public int getItemHeight() { - return Math.max(widget.getDimension().height(), resetButton != null ? resetButton.getHeight() : 0) + 2; - } - - @Override - public void setFocused(boolean focused) { - super.setFocused(focused); - } - - @Override - public List narratables() { - if (resetButton == null) - return ImmutableList.of(widget); - - return ImmutableList.of(widget, resetButton); - } - - @Override - public List children() { - if (resetButton == null) - return ImmutableList.of(widget); - - return ImmutableList.of(widget, resetButton); - } - } - - public class GroupSeparatorEntry extends Entry { - protected final OptionGroup group; - protected final MultiLineLabel wrappedName; - protected final MultiLineLabel wrappedTooltip; - - protected final LowProfileButtonWidget expandMinimizeButton; - - protected final Screen screen; - protected final Font font = Minecraft.getInstance().font; - - protected boolean groupExpanded; - - protected List childEntries = new ArrayList<>(); - - private int y; - - private GroupSeparatorEntry(OptionGroup group, Screen screen) { - this.group = group; - this.screen = screen; - this.wrappedName = MultiLineLabel.create(font, group.name(), getRowWidth() - 45); - this.wrappedTooltip = MultiLineLabel.create(font, group.tooltip(), screen.width / 3 * 2 - 10); - this.groupExpanded = !group.collapsed(); - this.expandMinimizeButton = new LowProfileButtonWidget(0, 0, 20, 20, Component.empty(), btn -> onExpandButtonPress()); - updateExpandMinimizeText(); - } - - @Override - public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - this.y = y; - - int buttonY = y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2 + 1; - - expandMinimizeButton.setY(buttonY); - expandMinimizeButton.setX(x); - expandMinimizeButton.render(matrices, mouseX, mouseY, tickDelta); - - wrappedName.renderCentered(matrices, x + entryWidth / 2, y + getYPadding()); - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - if ((isHovered() && !expandMinimizeButton.isMouseOver(mouseX, mouseY)) || expandMinimizeButton.isFocused()) { - YACLScreen.renderMultilineTooltip(matrices, font, wrappedTooltip, getRowLeft() + getRowWidth() / 2, y - 3, y + getItemHeight() + 3, screen.width, screen.height); - } - } - - public boolean isExpanded() { - return groupExpanded; - } - - public void setExpanded(boolean expanded) { - if (this.groupExpanded == expanded) - return; - - this.groupExpanded = expanded; - updateExpandMinimizeText(); - recacheViewableChildren(); - } - - protected void onExpandButtonPress() { - setExpanded(!isExpanded()); - } - - protected void updateExpandMinimizeText() { - expandMinimizeButton.setMessage(Component.literal(isExpanded() ? "▼" : "▶")); - } - - public void setChildEntries(List childEntries) { - this.childEntries.clear(); - this.childEntries.addAll(childEntries); - } - - @Override - public boolean isViewable() { - return yaclScreen.searchFieldWidget.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable); - } - - @Override - public int getItemHeight() { - return Math.max(wrappedName.getLineCount(), 1) * font.lineHeight + getYPadding() * 2; - } - - private int getYPadding() { - return 6; - } - - @Override - public List narratables() { - return ImmutableList.of(new NarratableEntry() { - @Override - public NarratableEntry.NarrationPriority narrationPriority() { - return NarrationPriority.HOVERED; - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - builder.add(NarratedElementType.TITLE, group.name()); - builder.add(NarratedElementType.HINT, group.tooltip()); - } - }); - } - - @Override - public List children() { - return ImmutableList.of(expandMinimizeButton); - } - } - - public class ListGroupSeparatorEntry extends GroupSeparatorEntry { - private final ListOption listOption; - private final TextScaledButtonWidget resetListButton; - private final TooltipButtonWidget addListButton; - - private ListGroupSeparatorEntry(ListOption group, Screen screen) { - super(group, screen); - this.listOption = group; - - this.resetListButton = new TextScaledButtonWidget(getRowRight() - 20, -50, 20, 20, 2f, Component.literal("\u21BB"), button -> { - group.requestSetDefault(); - }); - group.addListener((opt, val) -> this.resetListButton.active = !opt.isPendingValueDefault() && opt.available()); - this.resetListButton.active = !group.isPendingValueDefault() && group.available(); - - this.addListButton = new TooltipButtonWidget(yaclScreen, resetListButton.getX() - 20, -50, 20, 20, Component.literal("+"), Component.translatable("yacl.list.add_top"), btn -> { - group.insertNewEntryToTop(); - setExpanded(true); - }); - - updateExpandMinimizeText(); - minimizeIfUnavailable(); - } - - @Override - public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - updateExpandMinimizeText(); // update every render because option could become available/unavailable at any time - - super.render(matrices, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickDelta); - - int buttonY = expandMinimizeButton.getY(); - - resetListButton.setY(buttonY); - addListButton.setY(buttonY); - - resetListButton.render(matrices, mouseX, mouseY, tickDelta); - addListButton.render(matrices, mouseX, mouseY, tickDelta); - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - minimizeIfUnavailable(); // cannot run in render because it *should* cause a ConcurrentModificationException (but doesn't) - - super.postRender(matrices, mouseX, mouseY, delta); - - addListButton.renderHoveredTooltip(matrices); - } - - private void minimizeIfUnavailable() { - if (!listOption.available() && isExpanded()) { - setExpanded(false); - } - } - - @Override - protected void updateExpandMinimizeText() { - super.updateExpandMinimizeText(); - expandMinimizeButton.active = listOption == null || listOption.available(); - if (addListButton != null) - addListButton.active = expandMinimizeButton.active; - } - - @Override - public List children() { - return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton); - } - } - - public class EmptyListLabel extends Entry { - private final ListGroupSeparatorEntry parent; - private final String groupName; - private final String categoryName; - - public EmptyListLabel(ListGroupSeparatorEntry parent, ConfigCategory category) { - this.parent = parent; - this.groupName = parent.group.name().getString().toLowerCase(); - this.categoryName = category.name().getString().toLowerCase(); - } - - @Override - public void render(PoseStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { - drawCenteredString(matrices, Minecraft.getInstance().font, Component.translatable("yacl.list.empty").withStyle(ChatFormatting.DARK_GRAY, ChatFormatting.ITALIC), x + entryWidth / 2, y, -1); - } - - @Override - public boolean isViewable() { - String query = yaclScreen.searchFieldWidget.getQuery(); - return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty() - || (!singleCategory && categoryName.contains(query)) - || groupName.contains(query)); - } - - @Override - public int getItemHeight() { - return 11; - } - - @Override - public List children() { - return ImmutableList.of(); - } - - @Override - public List narratables() { - return ImmutableList.of(); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/RequireRestartScreen.java b/src/client/java/dev/isxander/yacl/gui/RequireRestartScreen.java deleted file mode 100644 index 18b6033..0000000 --- a/src/client/java/dev/isxander/yacl/gui/RequireRestartScreen.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.isxander.yacl.gui; - -import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ConfirmScreen; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; - -public class RequireRestartScreen extends ConfirmScreen { - public RequireRestartScreen(Screen parent) { - super(option -> { - if (option) Minecraft.getInstance().stop(); - else Minecraft.getInstance().setScreen(parent); - }, - Component.translatable("yacl.restart.title").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), - Component.translatable("yacl.restart.message"), - Component.translatable("yacl.restart.yes"), - Component.translatable("yacl.restart.no") - ); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java deleted file mode 100644 index 5cf38e0..0000000 --- a/src/client/java/dev/isxander/yacl/gui/SearchFieldWidget.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.EditBox; -import net.minecraft.network.chat.Component; - -public class SearchFieldWidget extends EditBox { - private Component emptyText; - private final YACLScreen yaclScreen; - private final Font font; - - private boolean isEmpty = true; - - public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText) { - super(font, x, y, width, height, text); - setResponder(string -> update()); - setFilter(string -> !string.endsWith(" ") && !string.startsWith(" ")); - this.yaclScreen = yaclScreen; - this.font = font; - this.emptyText = emptyText; - } - - @Override - public void renderWidget(PoseStack matrices, int mouseX, int mouseY, float delta) { - super.renderWidget(matrices, mouseX, mouseY, delta); - if (isVisible() && isEmpty()) { - font.drawShadow(matrices, emptyText, getX() + 4, this.getY() + (this.height - 8) / 2f, 0x707070); - } - } - - private void update() { - boolean wasEmpty = isEmpty; - isEmpty = getValue().isEmpty(); - - if (isEmpty && wasEmpty) - return; - - if (!isEmpty && yaclScreen.getCurrentCategoryIdx() != -1) - yaclScreen.changeCategory(-1); - if (isEmpty && yaclScreen.getCurrentCategoryIdx() == -1) - yaclScreen.changeCategory(0); - - yaclScreen.optionList.expandAllGroups(); - yaclScreen.optionList.recacheViewableChildren(); - - yaclScreen.optionList.setScrollAmount(0); - yaclScreen.categoryList.setScrollAmount(0); - } - - public String getQuery() { - return getValue().toLowerCase(); - } - - public boolean isEmpty() { - return isEmpty; - } - - public Component getEmptyText() { - return emptyText; - } - - public void setEmptyText(Component emptyText) { - this.emptyText = emptyText; - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java deleted file mode 100644 index b955912..0000000 --- a/src/client/java/dev/isxander/yacl/gui/TextScaledButtonWidget.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.Tooltip; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; - -public class TextScaledButtonWidget extends Button { - public float textScale; - - public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress) { - super(x, y, width, height, message, onPress, DEFAULT_NARRATION); - this.textScale = textScale; - } - - public TextScaledButtonWidget(int x, int y, int width, int height, float textScale, Component message, OnPress onPress, Tooltip tooltip) { - this(x, y, width, height, textScale, message, onPress); - setTooltip(tooltip); - } - - @Override - public void renderString(PoseStack matrices, Font textRenderer, int color) { - Font font = Minecraft.getInstance().font; - - matrices.pushPose(); - matrices.translate(((this.getX() + this.width / 2f) - font.width(getMessage()) * textScale / 2), (float)this.getY() + (this.height - 8 * textScale) / 2f / textScale, 0); - matrices.scale(textScale, textScale, 1); - font.drawShadow(matrices, getMessage(), 0, 0, color | Mth.ceil(this.alpha * 255.0F) << 24); - matrices.popPose(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java b/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java deleted file mode 100644 index 1395a3f..0000000 --- a/src/client/java/dev/isxander/yacl/gui/TooltipButtonWidget.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.vertex.PoseStack; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; - -public class TooltipButtonWidget extends TextScaledButtonWidget { - - protected final Screen screen; - protected MultiLineLabel wrappedDescription; - - public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, float textScale, Component tooltip, Button.OnPress onPress) { - super(x, y, width, height, textScale, message, onPress); - this.screen = screen; - setTooltip(tooltip); - } - - public TooltipButtonWidget(Screen screen, int x, int y, int width, int height, Component message, Component tooltip, Button.OnPress onPress) { - this(screen, x, y, width, height, message, 1, tooltip, onPress); - } - - public void renderHoveredTooltip(PoseStack matrices) { - if (isHoveredOrFocused()) { - YACLScreen.renderMultilineTooltip(matrices, Minecraft.getInstance().font, wrappedDescription, getX() + width / 2, getY() - 4, getY() + height + 4, screen.width, screen.height); - } - } - - public void setTooltip(Component tooltip) { - wrappedDescription = MultiLineLabel.create(Minecraft.getInstance().font, tooltip, screen.width / 3 - 5); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/YACLScreen.java b/src/client/java/dev/isxander/yacl/gui/YACLScreen.java deleted file mode 100644 index d653d8c..0000000 --- a/src/client/java/dev/isxander/yacl/gui/YACLScreen.java +++ /dev/null @@ -1,316 +0,0 @@ -package dev.isxander.yacl.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.*; -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.api.utils.MutableDimension; -import dev.isxander.yacl.api.utils.OptionUtils; -import dev.isxander.yacl.gui.utils.GuiUtils; -import dev.isxander.yacl.impl.utils.YACLConstants; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil; -import net.minecraft.client.renderer.GameRenderer; -import net.minecraft.network.chat.CommonComponents; -import net.minecraft.network.chat.Component; -import org.joml.Matrix4f; - -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; - -public class YACLScreen extends Screen { - public final YetAnotherConfigLib config; - private int currentCategoryIdx; - - private final Screen parent; - - public OptionListWidget optionList; - public CategoryListWidget categoryList; - public TooltipButtonWidget finishedSaveButton, cancelResetButton, undoButton; - public SearchFieldWidget searchFieldWidget; - - public Component saveButtonMessage, saveButtonTooltipMessage; - private int saveButtonMessageTime; - - - public YACLScreen(YetAnotherConfigLib config, Screen parent) { - super(config.title()); - this.config = config; - this.parent = parent; - this.currentCategoryIdx = 0; - } - - @Override - protected void init() { - int columnWidth = width / 3; - int padding = columnWidth / 20; - columnWidth = Math.min(columnWidth, 400); - int paddedWidth = columnWidth - padding * 2; - - MutableDimension actionDim = Dimension.ofInt(width / 3 / 2, height - padding - 20, paddedWidth, 20); - finishedSaveButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.empty(), - Component.empty(), - btn -> finishOrSave() - ); - actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22); - cancelResetButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.empty(), - Component.empty(), - btn -> cancelOrReset() - ); - actionDim.move(actionDim.width() + 4, 0); - undoButton = new TooltipButtonWidget( - this, - actionDim.x() - actionDim.width() / 2, - actionDim.y(), - actionDim.width(), - actionDim.height(), - Component.translatable("yacl.gui.undo"), - Component.translatable("yacl.gui.undo.tooltip"), - btn -> undo() - ); - - searchFieldWidget = new SearchFieldWidget( - this, - font, - width / 3 / 2 - paddedWidth / 2 + 1, - undoButton.getY() - 22, - paddedWidth - 2, 18, - Component.translatable("gui.recipebook.search_hint"), - Component.translatable("gui.recipebook.search_hint") - ); - - categoryList = new CategoryListWidget(minecraft, this, width, height); - addWidget(categoryList); - - updateActionAvailability(); - addRenderableWidget(searchFieldWidget); - addRenderableWidget(cancelResetButton); - addRenderableWidget(undoButton); - addRenderableWidget(finishedSaveButton); - - optionList = new OptionListWidget(this, minecraft, width, height); - addWidget(optionList); - - config.initConsumer().accept(this); - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - renderBackground(matrices); - - super.render(matrices, mouseX, mouseY, delta); - categoryList.render(matrices, mouseX, mouseY, delta); - searchFieldWidget.render(matrices, mouseX, mouseY, delta); - optionList.render(matrices, mouseX, mouseY, delta); - - categoryList.postRender(matrices, mouseX, mouseY, delta); - optionList.postRender(matrices, mouseX, mouseY, delta); - - for (GuiEventListener child : children()) { - if (child instanceof TooltipButtonWidget tooltipButtonWidget) { - tooltipButtonWidget.renderHoveredTooltip(matrices); - } - } - } - - protected void finishOrSave() { - saveButtonMessage = null; - - if (pendingChanges()) { - Set flags = new HashSet<>(); - OptionUtils.forEachOptions(config, option -> { - if (option.applyValue()) { - flags.addAll(option.flags()); - } - }); - OptionUtils.forEachOptions(config, option -> { - if (option.changed()) { - // if still changed after applying, reset to the current value from binding - // as something has gone wrong. - option.forgetPendingValue(); - YACLConstants.LOGGER.error("Option '{}' value mismatch after applying! Reset to binding's getter.", option.name().getString()); - } - }); - config.saveFunction().run(); - - flags.forEach(flag -> flag.accept(minecraft)); - } else onClose(); - } - - protected void cancelOrReset() { - if (pendingChanges()) { // if pending changes, button acts as a cancel button - OptionUtils.forEachOptions(config, Option::forgetPendingValue); - onClose(); - } else { // if not, button acts as a reset button - OptionUtils.forEachOptions(config, Option::requestSetDefault); - } - } - - protected void undo() { - OptionUtils.forEachOptions(config, Option::forgetPendingValue); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (optionList.keyPressed(keyCode, scanCode, modifiers)) { - return true; - } - - return super.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean charTyped(char chr, int modifiers) { - if (optionList.charTyped(chr, modifiers)) { - return true; - } - - return super.charTyped(chr, modifiers); - } - - public void changeCategory(int idx) { - if (idx == currentCategoryIdx) - return; - - if (idx != -1 && config.categories().get(idx) instanceof PlaceholderCategory placeholderCategory) { - minecraft.setScreen(placeholderCategory.screen().apply(minecraft, this)); - } else { - currentCategoryIdx = idx; - optionList.refreshOptions(); - } - } - - public int getCurrentCategoryIdx() { - return currentCategoryIdx; - } - - private void updateActionAvailability() { - boolean pendingChanges = pendingChanges(); - - undoButton.active = pendingChanges; - finishedSaveButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE)); - finishedSaveButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip")); - cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset")); - cancelResetButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip")); - } - - @Override - public void tick() { - searchFieldWidget.tick(); - - updateActionAvailability(); - - if (saveButtonMessage != null) { - if (saveButtonMessageTime > 140) { - saveButtonMessage = null; - saveButtonTooltipMessage = null; - saveButtonMessageTime = 0; - } else { - saveButtonMessageTime++; - finishedSaveButton.setMessage(saveButtonMessage); - if (saveButtonTooltipMessage != null) { - finishedSaveButton.setTooltip(saveButtonTooltipMessage); - } - } - } - } - - private void setSaveButtonMessage(Component message, Component tooltip) { - saveButtonMessage = message; - saveButtonTooltipMessage = tooltip; - saveButtonMessageTime = 0; - } - - private boolean pendingChanges() { - AtomicBoolean pendingChanges = new AtomicBoolean(false); - OptionUtils.consumeOptions(config, (option) -> { - if (option.changed()) { - pendingChanges.set(true); - return true; - } - return false; - }); - - return pendingChanges.get(); - } - - @Override - public boolean shouldCloseOnEsc() { - if (pendingChanges()) { - setSaveButtonMessage(Component.translatable("yacl.gui.save_before_exit").withStyle(ChatFormatting.RED), Component.translatable("yacl.gui.save_before_exit.tooltip")); - return false; - } - return true; - } - - @Override - public void onClose() { - minecraft.setScreen(parent); - } - - public static void renderMultilineTooltip(PoseStack matrices, Font font, MultiLineLabel text, int centerX, int yAbove, int yBelow, int screenWidth, int screenHeight) { - if (text.getLineCount() > 0) { - int maxWidth = text.getWidth(); - int lineHeight = font.lineHeight + 1; - int height = text.getLineCount() * lineHeight - 1; - - int belowY = yBelow + 12; - int aboveY = yAbove - height + 12; - int maxBelow = screenHeight - (belowY + height); - int minAbove = aboveY - height; - int y = aboveY; - if (minAbove < 8) - y = maxBelow > minAbove ? belowY : aboveY; - - int x = Math.max(centerX - text.getWidth() / 2 - 12, -6); - - int drawX = x + 12; - int drawY = y - 12; - - matrices.pushPose(); - Tesselator tesselator = Tesselator.getInstance(); - BufferBuilder bufferBuilder = tesselator.getBuilder(); - RenderSystem.setShader(GameRenderer::getPositionColorShader); - bufferBuilder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); - Matrix4f matrix4f = matrices.last().pose(); - TooltipRenderUtil.renderTooltipBackground( - GuiComponent::fillGradient, - matrix4f, - bufferBuilder, - drawX, - drawY, - maxWidth, - height, - 400 - ); - RenderSystem.enableDepthTest(); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - BufferUploader.drawWithShader(bufferBuilder.end()); - RenderSystem.disableBlend(); - matrices.translate(0.0, 0.0, 400.0); - - text.renderLeftAligned(matrices, drawX, drawY, lineHeight, -1); - - matrices.popPose(); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ActionController.java b/src/client/java/dev/isxander/yacl/gui/controllers/ActionController.java deleted file mode 100644 index e57cdd2..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/ActionController.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.mojang.blaze3d.platform.InputConstants; -import dev.isxander.yacl.api.ButtonOption; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.network.chat.Component; - -import java.util.function.BiConsumer; - -/** - * Simple controller that simply runs the button action on press - * and renders a {@link} Text on the right. - */ -public class ActionController implements Controller> { - public static final Component DEFAULT_TEXT = Component.translatable("yacl.control.action.execute"); - - private final ButtonOption option; - private final Component text; - - /** - * Constructs an action controller - * with the default formatter of {@link ActionController#DEFAULT_TEXT} - * - * @param option bound option - */ - public ActionController(ButtonOption option) { - this(option, DEFAULT_TEXT); - } - - /** - * Constructs an action controller - * - * @param option bound option - * @param text text to display - */ - public ActionController(ButtonOption option, Component text) { - this.option = option; - this.text = text; - - } - - /** - * {@inheritDoc} - */ - @Override - public ButtonOption option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return text; - } - - /** - * {@inheritDoc} - */ - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new ActionControllerElement(this, screen, widgetDimension); - } - - public static class ActionControllerElement extends ControllerWidget { - private final String buttonString; - - public ActionControllerElement(ActionController control, YACLScreen screen, Dimension dim) { - super(control, screen, dim); - buttonString = control.formatValue().getString().toLowerCase(); - } - - public void executeAction() { - playDownSound(); - control.option().action().accept(screen, control.option()); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (isMouseOver(mouseX, mouseY) && isAvailable()) { - executeAction(); - return true; - } - return false; - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused) { - return false; - } - - if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { - executeAction(); - return true; - } - - return false; - } - - @Override - protected int getHoveredControlWidth() { - return getUnhoveredControlWidth(); - } - - @Override - public boolean canReset() { - return false; - } - - @Override - public boolean matchesSearch(String query) { - return super.matchesSearch(query) || buttonString.contains(query); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/BooleanController.java b/src/client/java/dev/isxander/yacl/gui/controllers/BooleanController.java deleted file mode 100644 index d7d17d5..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/BooleanController.java +++ /dev/null @@ -1,157 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.CommonComponents; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * This controller renders a simple formatted {@link Component} - */ -public class BooleanController implements Controller { - - public static final Function ON_OFF_FORMATTER = (state) -> - state - ? CommonComponents.OPTION_ON - : CommonComponents.OPTION_OFF; - - public static final Function TRUE_FALSE_FORMATTER = (state) -> - state - ? Component.translatable("yacl.control.boolean.true") - : Component.translatable("yacl.control.boolean.false"); - - public static final Function YES_NO_FORMATTER = (state) -> - state - ? CommonComponents.GUI_YES - : CommonComponents.GUI_NO; - - private final Option option; - private final Function valueFormatter; - private final boolean coloured; - - /** - * Constructs a tickbox controller - * with the default value formatter of {@link BooleanController#ON_OFF_FORMATTER} - * - * @param option bound option - */ - public BooleanController(Option option) { - this(option, ON_OFF_FORMATTER, false); - } - - /** - * Constructs a tickbox controller - * with the default value formatter of {@link BooleanController#ON_OFF_FORMATTER} - * - * @param option bound option - * @param coloured value format is green or red depending on the state - */ - public BooleanController(Option option, boolean coloured) { - this(option, ON_OFF_FORMATTER, coloured); - } - - /** - * Constructs a tickbox controller - * - * @param option bound option - * @param valueFormatter format value into any {@link Component} - * @param coloured value format is green or red depending on the state - */ - public BooleanController(Option option, Function valueFormatter, boolean coloured) { - this.option = option; - this.valueFormatter = valueFormatter; - this.coloured = coloured; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * Value format is green or red depending on the state - */ - public boolean coloured() { - return coloured; - } - - /** - * {@inheritDoc} - */ - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new BooleanControllerElement(this, screen, widgetDimension); - } - - public static class BooleanControllerElement extends ControllerWidget { - public BooleanControllerElement(BooleanController control, YACLScreen screen, Dimension dim) { - super(control, screen, dim); - } - - @Override - protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { - - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!isMouseOver(mouseX, mouseY) || !isAvailable()) - return false; - - toggleSetting(); - return true; - } - - @Override - protected int getHoveredControlWidth() { - return getUnhoveredControlWidth(); - } - - public void toggleSetting() { - control.option().requestSet(!control.option().pendingValue()); - playDownSound(); - } - - @Override - protected Component getValueText() { - if (control.coloured()) { - return super.getValueText().copy().withStyle(control.option().pendingValue() ? ChatFormatting.GREEN : ChatFormatting.RED); - } - - return super.getValueText(); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!isFocused()) { - return false; - } - - if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { - toggleSetting(); - return true; - } - - return false; - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ColorController.java b/src/client/java/dev/isxander/yacl/gui/controllers/ColorController.java deleted file mode 100644 index 473407b..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/ColorController.java +++ /dev/null @@ -1,221 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.api.utils.MutableDimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.string.IStringController; -import dev.isxander.yacl.gui.controllers.string.StringControllerElement; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; - -import java.awt.Color; -import java.util.List; - -/** - * A color controller that uses a hex color field as input. - */ -public class ColorController implements IStringController { - private final Option option; - private final boolean allowAlpha; - - /** - * Constructs a color controller with {@link ColorController#allowAlpha()} defaulting to false - * - * @param option bound option - */ - public ColorController(Option option) { - this(option, false); - } - - /** - * Constructs a color controller - * - * @param option bound option - * @param allowAlpha allows the color input to accept alpha values - */ - public ColorController(Option option, boolean allowAlpha) { - this.option = option; - this.allowAlpha = allowAlpha; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - public boolean allowAlpha() { - return allowAlpha; - } - - @Override - public String getString() { - return formatValue().getString(); - } - - @Override - public Component formatValue() { - MutableComponent text = Component.literal("#"); - text.append(Component.literal(toHex(option().pendingValue().getRed())).withStyle(ChatFormatting.RED)); - text.append(Component.literal(toHex(option().pendingValue().getGreen())).withStyle(ChatFormatting.GREEN)); - text.append(Component.literal(toHex(option().pendingValue().getBlue())).withStyle(ChatFormatting.BLUE)); - if (allowAlpha()) text.append(toHex(option().pendingValue().getAlpha())); - return text; - } - - private String toHex(int value) { - String hex = Integer.toString(value, 16).toUpperCase(); - if (hex.length() == 1) - hex = "0" + hex; - return hex; - } - - @Override - public void setFromString(String value) { - if (value.startsWith("#")) - value = value.substring(1); - - int red = Integer.parseInt(value.substring(0, 2), 16); - int green = Integer.parseInt(value.substring(2, 4), 16); - int blue = Integer.parseInt(value.substring(4, 6), 16); - - if (allowAlpha()) { - int alpha = Integer.parseInt(value.substring(6, 8), 16); - option().requestSet(new Color(red, green, blue, alpha)); - } else { - option().requestSet(new Color(red, green, blue)); - } - } - - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new ColorControllerElement(this, screen, widgetDimension); - } - - public static class ColorControllerElement extends StringControllerElement { - private final ColorController colorController; - - protected MutableDimension colorPreviewDim; - - private final List allowedChars; - - public ColorControllerElement(ColorController control, YACLScreen screen, Dimension dim) { - super(control, screen, dim, true); - this.colorController = control; - this.allowedChars = ImmutableList.of('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'); - } - - @Override - protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { - if (isHovered()) { - colorPreviewDim.move(-inputFieldBounds.width() - 5, 0); - super.drawValueText(matrices, mouseX, mouseY, delta); - } - - GuiComponent.fill(matrices, colorPreviewDim.x(), colorPreviewDim.y(), colorPreviewDim.xLimit(), colorPreviewDim.yLimit(), colorController.option().pendingValue().getRGB()); - drawOutline(matrices, colorPreviewDim.x(), colorPreviewDim.y(), colorPreviewDim.xLimit(), colorPreviewDim.yLimit(), 1, 0xFF000000); - } - - @Override - public void write(String string) { - if (string.startsWith("0x")) string = string.substring(2); - for (char chr : string.toCharArray()) { - if (!allowedChars.contains(Character.toLowerCase(chr))) { - return; - } - } - - if (caretPos == 0) - return; - - String trimmed = string.substring(0, Math.min(inputField.length() - caretPos, string.length())); - - if (modifyInput(builder -> builder.replace(caretPos, caretPos + trimmed.length(), trimmed))) { - caretPos += trimmed.length(); - setSelectionLength(); - updateControl(); - } - } - - @Override - protected void doBackspace() { - if (caretPos > 1) { - if (modifyInput(builder -> builder.setCharAt(caretPos - 1, '0'))) { - caretPos--; - updateControl(); - } - } - } - - @Override - protected void doDelete() { - if (caretPos >= 1) { - if (modifyInput(builder -> builder.setCharAt(caretPos, '0'))) { - updateControl(); - } - } - } - - @Override - protected boolean doCut() { - return false; - } - - @Override - protected boolean doCopy() { - return false; - } - - @Override - protected boolean doSelectAll() { - return false; - } - - protected void setSelectionLength() { - selectionLength = caretPos < inputField.length() && caretPos > 0 ? 1 : 0; - } - - @Override - protected int getDefaultCaretPos() { - return colorController.allowAlpha() ? 3 : 1; - } - - @Override - public void setDimension(Dimension dim) { - super.setDimension(dim); - - int previewSize = (dim.height() - getYPadding() * 2) / 2; - colorPreviewDim = Dimension.ofInt(dim.xLimit() - getXPadding() - previewSize, dim.centerY() - previewSize / 2, previewSize, previewSize); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - int prevSelectionLength = selectionLength; - selectionLength = 0; - if (super.keyPressed(keyCode, scanCode, modifiers)) { - caretPos = Math.max(1, caretPos); - setSelectionLength(); - return true; - } else selectionLength = prevSelectionLength; - return false; - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (super.mouseClicked(mouseX, mouseY, button)) { - caretPos = Math.max(1, caretPos); - setSelectionLength(); - return true; - } - return false; - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java b/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java deleted file mode 100644 index 22c469d..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java +++ /dev/null @@ -1,170 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.utils.GuiUtils; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.ComponentPath; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.narration.NarratedElementType; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.gui.navigation.FocusNavigationEvent; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.Nullable; - -public abstract class ControllerWidget> extends AbstractWidget { - protected final T control; - protected MultiLineLabel wrappedTooltip; - protected final YACLScreen screen; - - protected boolean focused = false; - protected boolean hovered = false; - - protected final Component modifiedOptionName; - protected final String optionNameString; - - public ControllerWidget(T control, YACLScreen screen, Dimension dim) { - super(dim); - this.control = control; - this.screen = screen; - control.option().addListener((opt, pending) -> updateTooltip()); - updateTooltip(); - this.modifiedOptionName = control.option().name().copy().withStyle(ChatFormatting.ITALIC); - this.optionNameString = control.option().name().getString().toLowerCase(); - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - hovered = isMouseOver(mouseX, mouseY); - - Component name = control.option().changed() ? modifiedOptionName : control.option().name(); - Component shortenedName = Component.literal(GuiUtils.shortenString(name.getString(), textRenderer, getDimension().width() - getControlWidth() - getXPadding() - 7, "...")).setStyle(name.getStyle()); - - drawButtonRect(matrices, getDimension().x(), getDimension().y(), getDimension().xLimit(), getDimension().yLimit(), isHovered(), isAvailable()); - matrices.pushPose(); - matrices.translate(getDimension().x() + getXPadding(), getTextY(), 0); - textRenderer.drawShadow(matrices, shortenedName, 0, 0, getValueColor()); - matrices.popPose(); - - drawValueText(matrices, mouseX, mouseY, delta); - if (isHovered()) { - drawHoveredControl(matrices, mouseX, mouseY, delta); - } - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - if (hovered || focused) { - YACLScreen.renderMultilineTooltip(matrices, textRenderer, wrappedTooltip, getDimension().centerX(), getDimension().y() - 5, getDimension().yLimit() + 5, screen.width, screen.height); - } - } - - protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { - - } - - protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { - Component valueText = getValueText(); - matrices.pushPose(); - matrices.translate(getDimension().xLimit() - textRenderer.width(valueText) - getXPadding(), getTextY(), 0); - textRenderer.drawShadow(matrices, valueText, 0, 0, getValueColor()); - matrices.popPose(); - } - - private void updateTooltip() { - this.wrappedTooltip = MultiLineLabel.create(textRenderer, control.option().tooltip(), screen.width / 3 * 2 - 10); - } - - protected int getControlWidth() { - return isHovered() ? getHoveredControlWidth() : getUnhoveredControlWidth(); - } - - public boolean isHovered() { - return isAvailable() && (hovered || focused); - } - - protected abstract int getHoveredControlWidth(); - - protected int getUnhoveredControlWidth() { - return textRenderer.width(getValueText()); - } - - protected int getXPadding() { - return 5; - } - - protected int getYPadding() { - return 2; - } - - protected Component getValueText() { - return control.formatValue(); - } - - protected boolean isAvailable() { - return control.option().available(); - } - - protected int getValueColor() { - return isAvailable() ? -1 : inactiveColor; - } - - @Override - public boolean canReset() { - return true; - } - - protected void drawOutline(PoseStack matrices, int x1, int y1, int x2, int y2, int width, int color) { - GuiComponent.fill(matrices, x1, y1, x2, y1 + width, color); - GuiComponent.fill(matrices, x2, y1, x2 - width, y2, color); - GuiComponent.fill(matrices, x1, y2, x2, y2 - width, color); - GuiComponent.fill(matrices, x1, y1, x1 + width, y2, color); - } - - protected float getTextY() { - return getDimension().y() + getDimension().height() / 2f - textRenderer.lineHeight / 2f; - } - - @Nullable - @Override - public ComponentPath nextFocusPath(FocusNavigationEvent focusNavigationEvent) { - if (!this.isAvailable()) - return null; - return !this.isFocused() ? ComponentPath.leaf(this) : null; - } - - @Override - public boolean isFocused() { - return focused; - } - - @Override - public void setFocused(boolean focused) { - this.focused = focused; - } - - @Override - public void unfocus() { - this.focused = false; - } - - @Override - public boolean matchesSearch(String query) { - return optionNameString.contains(query.toLowerCase()); - } - - @Override - public NarrationPriority narrationPriority() { - return focused ? NarrationPriority.FOCUSED : isHovered() ? NarrationPriority.HOVERED : NarrationPriority.NONE; - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - builder.add(NarratedElementType.TITLE, control.option().name()); - builder.add(NarratedElementType.HINT, control.option().tooltip()); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java b/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java deleted file mode 100644 index 1dfae0b..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/LabelController.java +++ /dev/null @@ -1,193 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.client.gui.ComponentPath; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.components.MultiLineLabel; -import net.minecraft.client.gui.narration.NarratedElementType; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.client.gui.navigation.FocusNavigationEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.HoverEvent; -import net.minecraft.network.chat.Style; -import net.minecraft.util.FormattedCharSequence; -import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -/** - * Simply renders some text as a label. - */ -public class LabelController implements Controller { - private final Option option; - /** - * Constructs a label controller - * - * @param option bound option - */ - public LabelController(Option option) { - this.option = option; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - @Override - public Component formatValue() { - return option().pendingValue(); - } - - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new LabelControllerElement(screen, widgetDimension); - } - - public class LabelControllerElement extends AbstractWidget { - private List wrappedText; - protected MultiLineLabel wrappedTooltip; - protected boolean focused; - - protected final YACLScreen screen; - - public LabelControllerElement(YACLScreen screen, Dimension dim) { - super(dim); - this.screen = screen; - option().addListener((opt, pending) -> updateTooltip()); - updateTooltip(); - updateText(); - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - updateText(); - - float y = getDimension().y(); - for (FormattedCharSequence text : wrappedText) { - textRenderer.drawShadow(matrices, text, getDimension().x() + getXPadding(), y + getYPadding(), option().available() ? -1 : 0xFFA0A0A0); - y += textRenderer.lineHeight; - } - - if (isFocused()) { - GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().y() - 1, getDimension().xLimit() + 1, getDimension().y(), -1); - GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().y() - 1, getDimension().x(), getDimension().yLimit() + 1, -1); - GuiComponent.fill(matrices, getDimension().x() - 1, getDimension().yLimit(), getDimension().xLimit() + 1, getDimension().yLimit() + 1, -1); - GuiComponent.fill(matrices, getDimension().xLimit(), getDimension().y() - 1, getDimension().xLimit() + 1, getDimension().yLimit() + 1, -1); - } - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - if (isMouseOver(mouseX, mouseY)) { - YACLScreen.renderMultilineTooltip(matrices, textRenderer, wrappedTooltip, getDimension().centerX(), getDimension().y() - 5, getDimension().yLimit() + 5, screen.width, screen.height); - - Style style = getStyle(mouseX, mouseY); - if (style != null && style.getHoverEvent() != null) { - HoverEvent hoverEvent = style.getHoverEvent(); - HoverEvent.ItemStackInfo itemStackContent = hoverEvent.getValue(HoverEvent.Action.SHOW_ITEM); - if (itemStackContent != null) { - ItemStack stack = itemStackContent.getItemStack(); - screen.renderTooltip(matrices, screen.getTooltipFromItem(stack), stack.getTooltipImage(), mouseX, mouseY); - } else { - HoverEvent.EntityTooltipInfo entityContent = hoverEvent.getValue(HoverEvent.Action.SHOW_ENTITY); - if (entityContent != null) { - if (this.client.options.advancedItemTooltips) { - screen.renderComponentTooltip(matrices, entityContent.getTooltipLines(), mouseX, mouseY); - } - } else { - Component text = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT); - if (text != null) { - MultiLineLabel multilineText = MultiLineLabel.create(textRenderer, text, getDimension().width()); - YACLScreen.renderMultilineTooltip(matrices, textRenderer, multilineText, getDimension().centerX(), getDimension().y(), getDimension().yLimit(), screen.width, screen.height); - } - } - } - } - } - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!isMouseOver(mouseX, mouseY)) - return false; - - Style style = getStyle((int) mouseX, (int) mouseY); - return screen.handleComponentClicked(style); - } - - protected Style getStyle(int mouseX, int mouseY) { - if (!getDimension().isPointInside(mouseX, mouseY)) - return null; - - int x = mouseX - getDimension().x(); - int y = mouseY - getDimension().y() - getYPadding(); - int line = y / textRenderer.lineHeight; - - if (x < 0 || x > getDimension().xLimit()) return null; - if (y < 0 || y > getDimension().yLimit()) return null; - if (line < 0 || line >= wrappedText.size()) return null; - - return textRenderer.getSplitter().componentStyleAtWidth(wrappedText.get(line), x); - } - - private int getXPadding() { - return 4; - } - - private int getYPadding() { - return 3; - } - - private void updateText() { - wrappedText = textRenderer.split(formatValue(), getDimension().width() - getXPadding() * 2); - setDimension(getDimension().withHeight(wrappedText.size() * textRenderer.lineHeight + getYPadding() * 2)); - } - - private void updateTooltip() { - this.wrappedTooltip = MultiLineLabel.create(textRenderer, option().tooltip(), screen.width / 3 * 2 - 10); - } - - @Override - public boolean matchesSearch(String query) { - return formatValue().getString().toLowerCase().contains(query.toLowerCase()); - } - - @Nullable - @Override - public ComponentPath nextFocusPath(FocusNavigationEvent focusNavigationEvent) { - if (!option().available()) - return null; - return !this.isFocused() ? ComponentPath.leaf(this) : null; - } - - @Override - public boolean isFocused() { - return focused; - } - - @Override - public void setFocused(boolean focused) { - this.focused = focused; - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - builder.add(NarratedElementType.TITLE, formatValue()); - } - - @Override - public NarrationPriority narrationPriority() { - return NarrationPriority.FOCUSED; - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java b/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java deleted file mode 100644 index 1acccf1..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/ListEntryWidget.java +++ /dev/null @@ -1,133 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.google.common.collect.ImmutableList; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.ListOption; -import dev.isxander.yacl.api.ListOptionEntry; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.*; -import net.minecraft.client.gui.components.events.ContainerEventHandler; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarrationElementOutput; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public class ListEntryWidget extends AbstractWidget implements ContainerEventHandler { - private final TooltipButtonWidget removeButton, moveUpButton, moveDownButton; - private final AbstractWidget entryWidget; - - private final ListOption listOption; - private final ListOptionEntry listOptionEntry; - - private final String optionNameString; - - private GuiEventListener focused; - private boolean dragging; - - public ListEntryWidget(YACLScreen screen, ListOptionEntry listOptionEntry, AbstractWidget entryWidget) { - super(entryWidget.getDimension().withHeight(Math.max(entryWidget.getDimension().height(), 20) - ((listOptionEntry.parentGroup().indexOf(listOptionEntry) == listOptionEntry.parentGroup().options().size() - 1) ? 0 : 2))); // -2 to remove the padding - this.listOptionEntry = listOptionEntry; - this.listOption = listOptionEntry.parentGroup(); - this.optionNameString = listOptionEntry.name().getString().toLowerCase(); - this.entryWidget = entryWidget; - - Dimension dim = entryWidget.getDimension(); - entryWidget.setDimension(dim.clone().move(20 * 2, 0).expand(-20 * 3, 0)); - - removeButton = new TooltipButtonWidget(screen, dim.xLimit() - 20, dim.y(), 20, 20, Component.literal("\u274c"), Component.translatable("yacl.list.remove"), btn -> { - listOption.removeEntry(listOptionEntry); - updateButtonStates(); - }); - - moveUpButton = new TooltipButtonWidget(screen, dim.x(), dim.y(), 20, 20, Component.literal("\u2191"), Component.translatable("yacl.list.move_up"), btn -> { - int index = listOption.indexOf(listOptionEntry) - 1; - if (index >= 0) { - listOption.removeEntry(listOptionEntry); - listOption.insertEntry(index, listOptionEntry); - updateButtonStates(); - } - }); - - moveDownButton = new TooltipButtonWidget(screen, dim.x() + 20, dim.y(), 20, 20, Component.literal("\u2193"), Component.translatable("yacl.list.move_down"), btn -> { - int index = listOption.indexOf(listOptionEntry) + 1; - if (index < listOption.options().size()) { - listOption.removeEntry(listOptionEntry); - listOption.insertEntry(index, listOptionEntry); - updateButtonStates(); - } - }); - - updateButtonStates(); - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - updateButtonStates(); // update every render in case option becomes available/unavailable - - removeButton.setY(getDimension().y()); - moveUpButton.setY(getDimension().y()); - moveDownButton.setY(getDimension().y()); - entryWidget.setDimension(entryWidget.getDimension().withY(getDimension().y())); - - removeButton.render(matrices, mouseX, mouseY, delta); - moveUpButton.render(matrices, mouseX, mouseY, delta); - moveDownButton.render(matrices, mouseX, mouseY, delta); - entryWidget.render(matrices, mouseX, mouseY, delta); - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - removeButton.renderHoveredTooltip(matrices); - moveUpButton.renderHoveredTooltip(matrices); - moveDownButton.renderHoveredTooltip(matrices); - } - - protected void updateButtonStates() { - removeButton.active = listOption.available(); - moveUpButton.active = listOption.indexOf(listOptionEntry) > 0 && listOption.available(); - moveDownButton.active = listOption.indexOf(listOptionEntry) < listOption.options().size() - 1 && listOption.available(); - } - - @Override - public void unfocus() { - entryWidget.unfocus(); - } - - @Override - public void updateNarration(NarrationElementOutput builder) { - entryWidget.updateNarration(builder); - } - - @Override - public boolean matchesSearch(String query) { - return optionNameString.contains(query.toLowerCase()); - } - - @Override - public List children() { - return ImmutableList.of(moveUpButton, moveDownButton, entryWidget, removeButton); - } - - @Override - public boolean isDragging() { - return dragging; - } - - @Override - public void setDragging(boolean dragging) { - this.dragging = dragging; - } - - @Nullable - @Override - public GuiEventListener getFocused() { - return focused; - } - - @Override - public void setFocused(@Nullable GuiEventListener focused) { - this.focused = focused; - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/TickBoxController.java b/src/client/java/dev/isxander/yacl/gui/controllers/TickBoxController.java deleted file mode 100644 index 3f615db..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/TickBoxController.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.isxander.yacl.gui.controllers; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.network.chat.Component; - -/** - * This controller renders a tickbox - */ -public class TickBoxController implements Controller { - private final Option option; - - /** - * Constructs a tickbox controller - * - * @param option bound option - */ - public TickBoxController(Option option) { - this.option = option; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return Component.empty(); - } - - /** - * {@inheritDoc} - */ - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new TickBoxControllerElement(this, screen, widgetDimension); - } - - public static class TickBoxControllerElement extends ControllerWidget { - public TickBoxControllerElement(TickBoxController control, YACLScreen screen, Dimension dim) { - super(control, screen, dim); - } - - @Override - protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { - int outlineSize = 10; - int outlineX1 = getDimension().xLimit() - getXPadding() - outlineSize; - int outlineY1 = getDimension().centerY() - outlineSize / 2; - int outlineX2 = getDimension().xLimit() - getXPadding(); - int outlineY2 = getDimension().centerY() + outlineSize / 2; - - int color = getValueColor(); - int shadowColor = multiplyColor(color, 0.25f); - - drawOutline(matrices, outlineX1 + 1, outlineY1 + 1, outlineX2 + 1, outlineY2 + 1, 1, shadowColor); - drawOutline(matrices, outlineX1, outlineY1, outlineX2, outlineY2, 1, color); - if (control.option().pendingValue()) { - GuiComponent.fill(matrices, outlineX1 + 3, outlineY1 + 3, outlineX2 - 1, outlineY2 - 1, shadowColor); - GuiComponent.fill(matrices, outlineX1 + 2, outlineY1 + 2, outlineX2 - 2, outlineY2 - 2, color); - } - } - - @Override - protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { - if (!isHovered()) - drawHoveredControl(matrices, mouseX, mouseY, delta); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!isMouseOver(mouseX, mouseY) || !isAvailable()) - return false; - - toggleSetting(); - return true; - } - - @Override - protected int getHoveredControlWidth() { - return 10; - } - - @Override - protected int getUnhoveredControlWidth() { - return 10; - } - - public void toggleSetting() { - control.option().requestSet(!control.option().pendingValue()); - playDownSound(); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused) { - return false; - } - - if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_SPACE || keyCode == InputConstants.KEY_NUMPADENTER) { - toggleSetting(); - return true; - } - - return false; - } - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java b/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java deleted file mode 100644 index ff20039..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingControllerElement.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.isxander.yacl.gui.controllers.cycling; - -import com.mojang.blaze3d.platform.InputConstants; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ControllerWidget; -import net.minecraft.client.gui.screens.Screen; - -public class CyclingControllerElement extends ControllerWidget> { - - public CyclingControllerElement(ICyclingController control, YACLScreen screen, Dimension dim) { - super(control, screen, dim); - } - - public void cycleValue(int increment) { - int targetIdx = control.getPendingValue() + increment; - if (targetIdx >= control.getCycleLength()) { - targetIdx -= control.getCycleLength(); - } else if (targetIdx < 0) { - targetIdx += control.getCycleLength(); - } - control.setPendingValue(targetIdx); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!isMouseOver(mouseX, mouseY) || (button != 0 && button != 1) || !isAvailable()) - return false; - - playDownSound(); - cycleValue(button == 1 || Screen.hasShiftDown() || Screen.hasControlDown() ? -1 : 1); - - return true; - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused) - return false; - - switch (keyCode) { - case InputConstants.KEY_LEFT -> - cycleValue(-1); - case InputConstants.KEY_RIGHT -> - cycleValue(1); - case InputConstants.KEY_RETURN, InputConstants.KEY_SPACE, InputConstants.KEY_NUMPADENTER -> - cycleValue(Screen.hasControlDown() || Screen.hasShiftDown() ? -1 : 1); - default -> { - return false; - } - } - - return true; - } - - @Override - protected int getHoveredControlWidth() { - return getUnhoveredControlWidth(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java b/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java deleted file mode 100644 index 34f2cc9..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/CyclingListController.java +++ /dev/null @@ -1,79 +0,0 @@ -package dev.isxander.yacl.gui.controllers.cycling; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * A controller where once clicked, cycles through elements - * in the provided list. - */ -public class CyclingListController implements ICyclingController { - private final Option option; - private final Function valueFormatter; - private final ImmutableList values; - - /** - * Constructs a {@link CyclingListController}, with a default - * value formatter of {@link Object#toString()}. - * @param option option of which to bind the controller to - * @param values the values to cycle through - */ - public CyclingListController(Option option, Iterable values) { - this(option, values, value -> Component.literal(value.toString())); - } - - /** - * Constructs a {@link CyclingListController} - * @param option option of which to bind the controller to - * @param values the values to cycle through - * @param valueFormatter function of how to convert each value to a string to display - */ - public CyclingListController(Option option, Iterable values, Function valueFormatter) { - this.option = option; - this.valueFormatter = valueFormatter; - this.values = ImmutableList.copyOf(values); - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(int ordinal) { - option().requestSet(values.get(ordinal)); - } - - /** - * {@inheritDoc} - */ - @Override - public int getPendingValue() { - return values.indexOf(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public int getCycleLength() { - return values.size(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java b/src/client/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java deleted file mode 100644 index ebd2cb6..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/EnumController.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.isxander.yacl.gui.controllers.cycling; - -import dev.isxander.yacl.api.NameableEnum; -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; -import net.minecraft.util.OptionEnum; - -import java.util.Arrays; -import java.util.function.Function; - -/** - * Simple controller type that displays the enum on the right. - *

- * Cycles forward with left click, cycles backward with right click or when shift is held - * - * @param enum type - */ -public class EnumController> extends CyclingListController { - public static > Function getDefaultFormatter() { - return value -> { - if (value instanceof NameableEnum nameableEnum) - return nameableEnum.getDisplayName(); - if (value instanceof OptionEnum translatableOption) - return translatableOption.getCaption(); - return Component.literal(value.toString()); - }; - } - - /** - * Constructs a cycling enum controller with a default value formatter and all values being available. - * The default value formatter first searches if the - * enum is a {@link NameableEnum} or {@link OptionEnum} else, just uses {@link Enum#toString()} - * - * @param option bound option - */ - public EnumController(Option option) { - this(option, getDefaultFormatter()); - } - - /** - * Constructs a cycling enum controller with all values being available. - * - * @param option bound option - * @param valueFormatter format the enum into any {@link Component} - */ - public EnumController(Option option, Function valueFormatter) { - this(option, valueFormatter, option.typeClass().getEnumConstants()); - } - - /** - * Constructs a cycling enum controller. - * - * @param option bound option - * @param valueFormatter format the enum into any {@link Component} - * @param availableValues all enum constants that can be cycled through - */ - public EnumController(Option option, Function valueFormatter, T[] availableValues) { - super(option, Arrays.asList(availableValues), valueFormatter); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java b/src/client/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java deleted file mode 100644 index 081b572..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/cycling/ICyclingController.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.isxander.yacl.gui.controllers.cycling; - -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; - -/** - * This interface simply generifies setting and getting of - * the pending value, using an ordinal so elements can cycle through - * without knowing the content. - */ -public interface ICyclingController extends Controller { - /** - * Sets the pending value to whatever corresponds to the ordinal - * @param ordinal index of element to set - */ - void setPendingValue(int ordinal); - - /** - * Gets the pending ordinal that corresponds to the actual value - * @return ordinal - */ - int getPendingValue(); - - /** - * Allows the element when it should wrap-around back to zeroth ordinal - */ - int getCycleLength(); - - /** - * {@inheritDoc} - */ - @Override - default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new CyclingControllerElement(this, screen, widgetDimension); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/package-info.java b/src/client/java/dev/isxander/yacl/gui/controllers/package-info.java deleted file mode 100644 index 12ce86b..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/package-info.java +++ /dev/null @@ -1,12 +0,0 @@ -/** - * This package contains all {@link dev.isxander.yacl.api.Controller} implementations - * - *

    - *
  • For numbers: {@link dev.isxander.yacl.gui.controllers.slider}
  • - *
  • For booleans: {@link dev.isxander.yacl.gui.controllers.TickBoxController}
  • - *
  • For lists/enums: {@link dev.isxander.yacl.gui.controllers.cycling}
  • - *
  • For strings: {@link dev.isxander.yacl.gui.controllers.string.StringController}
  • - *
  • For {@link dev.isxander.yacl.api.ButtonOption}: {@link dev.isxander.yacl.gui.controllers.ActionController}
  • - *
- */ -package dev.isxander.yacl.gui.controllers; diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java deleted file mode 100644 index 8e044b1..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/DoubleSliderController.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; -import org.apache.commons.lang3.Validate; - -import java.util.function.Function; - -/** - * {@link ISliderController} for doubles. - */ -public class DoubleSliderController implements ISliderController { - /** - * Formats doubles to two decimal places - */ - public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,.2f", value).replaceAll("[\u00a0\u202F]", " ")); - - private final Option option; - - private final double min, max, interval; - - private final Function valueFormatter; - - /** - * Constructs a {@link ISliderController} for doubles - * using the default value formatter {@link DoubleSliderController#DEFAULT_FORMATTER}. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - */ - public DoubleSliderController(Option option, double min, double max, double interval) { - this(option, min, max, interval, DEFAULT_FORMATTER); - } - - /** - * Constructs a {@link ISliderController} for doubles. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - * @param valueFormatter format the value into any {@link Component} - */ - public DoubleSliderController(Option option, double min, double max, double interval, Function valueFormatter) { - Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); - Validate.isTrue(interval > 0, "`interval` must be more than 0"); - Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); - - this.option = option; - this.min = min; - this.max = max; - this.interval = interval; - this.valueFormatter = valueFormatter; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return max; - } - - /** - * {@inheritDoc} - */ - @Override - public double interval() { - return interval; - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet(value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } - -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java deleted file mode 100644 index 25f2206..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/FloatSliderController.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; -import org.apache.commons.lang3.Validate; - -import java.util.function.Function; - -/** - * {@link ISliderController} for floats. - */ -public class FloatSliderController implements ISliderController { - /** - * Formats floats to one decimal place - */ - public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,.1f", value).replaceAll("[\u00a0\u202F]", " ")); - - private final Option option; - - private final float min, max, interval; - - private final Function valueFormatter; - - /** - * Constructs a {@link ISliderController} for floats - * using the default value formatter {@link FloatSliderController#DEFAULT_FORMATTER}. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - */ - public FloatSliderController(Option option, float min, float max, float interval) { - this(option, min, max, interval, DEFAULT_FORMATTER); - } - - /** - * Constructs a {@link ISliderController} for floats. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - * @param valueFormatter format the value into any {@link Component} - */ - public FloatSliderController(Option option, float min, float max, float interval, Function valueFormatter) { - Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); - Validate.isTrue(interval > 0, "`interval` must be more than 0"); - Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); - - this.option = option; - this.min = min; - this.max = max; - this.interval = interval; - this.valueFormatter = valueFormatter; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return max; - } - - /** - * {@inheritDoc} - */ - @Override - public double interval() { - return interval; - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((float) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } - -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java deleted file mode 100644 index aa3c18f..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; - -/** - * Simple custom slider implementation that shifts the current value across when shown. - *

- * For simplicity, {@link SliderControllerElement} works in doubles so each - * {@link ISliderController} must cast to double. This is to get around re-writing the element for every type. - */ -public interface ISliderController extends Controller { - /** - * Gets the minimum value for the slider - */ - double min(); - - /** - * Gets the maximum value for the slider - */ - double max(); - - /** - * Gets the interval (or step size) for the slider. - */ - double interval(); - - /** - * Gets the range of the slider. - */ - default double range() { - return max() - min(); - } - - /** - * Sets the {@link dev.isxander.yacl.api.Option}'s pending value - */ - void setPendingValue(double value); - - /** - * Gets the {@link dev.isxander.yacl.api.Option}'s pending value - */ - double pendingValue(); - - /** - * {@inheritDoc} - */ - @Override - default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new SliderControllerElement(this, screen, widgetDimension, min(), max(), interval()); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java deleted file mode 100644 index 4a68497..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/IntegerSliderController.java +++ /dev/null @@ -1,111 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; -import org.apache.commons.lang3.Validate; - -import java.util.function.Function; - -/** - * {@link ISliderController} for integers. - */ -public class IntegerSliderController implements ISliderController { - public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,d", value).replaceAll("[\u00a0\u202F]", " ")); - - private final Option option; - - private final int min, max, interval; - - private final Function valueFormatter; - - /** - * Constructs a {@link ISliderController} for integers - * using the default value formatter {@link IntegerSliderController#DEFAULT_FORMATTER}. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - */ - public IntegerSliderController(Option option, int min, int max, int interval) { - this(option, min, max, interval, DEFAULT_FORMATTER); - } - - /** - * Constructs a {@link ISliderController} for integers. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - * @param valueFormatter format the value into any {@link Component} - */ - public IntegerSliderController(Option option, int min, int max, int interval, Function valueFormatter) { - Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); - Validate.isTrue(interval > 0, "`interval` must be more than 0"); - Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); - - this.option = option; - this.min = min; - this.max = max; - this.interval = interval; - this.valueFormatter = valueFormatter; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return max; - } - - /** - * {@inheritDoc} - */ - @Override - public double interval() { - return interval; - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((int) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } - -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java deleted file mode 100644 index 681e7cf..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/LongSliderController.java +++ /dev/null @@ -1,111 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import dev.isxander.yacl.api.Option; -import net.minecraft.network.chat.Component; -import org.apache.commons.lang3.Validate; - -import java.util.function.Function; - -/** - * {@link ISliderController} for longs. - */ -public class LongSliderController implements ISliderController { - public static final Function DEFAULT_FORMATTER = value -> Component.literal(String.format("%,d", value).replaceAll("[\u00a0\u202F]", " ")); - - private final Option option; - - private final long min, max, interval; - - private final Function valueFormatter; - - /** - * Constructs a {@link ISliderController} for longs - * using the default value formatter {@link LongSliderController#DEFAULT_FORMATTER}. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - */ - public LongSliderController(Option option, long min, long max, long interval) { - this(option, min, max, interval, DEFAULT_FORMATTER); - } - - /** - * Constructs a {@link ISliderController} for longs. - * - * @param option bound option - * @param min minimum slider value - * @param max maximum slider value - * @param interval step size (or increments) for the slider - * @param valueFormatter format the value into any {@link Component} - */ - public LongSliderController(Option option, long min, long max, long interval, Function valueFormatter) { - Validate.isTrue(max > min, "`max` cannot be smaller than `min`"); - Validate.isTrue(interval > 0, "`interval` must be more than 0"); - Validate.notNull(valueFormatter, "`valueFormatter` must not be null"); - - this.option = option; - this.min = min; - this.max = max; - this.interval = interval; - this.valueFormatter = valueFormatter; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - /** - * {@inheritDoc} - */ - @Override - public Component formatValue() { - return valueFormatter.apply(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return max; - } - - /** - * {@inheritDoc} - */ - @Override - public double interval() { - return interval; - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((long) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } - -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java deleted file mode 100644 index d00f3d7..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java +++ /dev/null @@ -1,164 +0,0 @@ -package dev.isxander.yacl.gui.controllers.slider; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ControllerWidget; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.util.Mth; - -public class SliderControllerElement extends ControllerWidget> { - private final double min, max, interval; - - private float interpolation; - - private Dimension sliderBounds; - - private boolean mouseDown = false; - - public SliderControllerElement(ISliderController option, YACLScreen screen, Dimension dim, double min, double max, double interval) { - super(option, screen, dim); - this.min = min; - this.max = max; - this.interval = interval; - setDimension(dim); - } - - @Override - public void render(PoseStack matrices, int mouseX, int mouseY, float delta) { - super.render(matrices, mouseX, mouseY, delta); - - calculateInterpolation(); - } - - @Override - protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { - // track - GuiComponent.fill(matrices, sliderBounds.x(), sliderBounds.centerY() - 1, sliderBounds.xLimit(), sliderBounds.centerY(), -1); - // track shadow - GuiComponent.fill(matrices, sliderBounds.x() + 1, sliderBounds.centerY(), sliderBounds.xLimit() + 1, sliderBounds.centerY() + 1, 0xFF404040); - - // thumb shadow - GuiComponent.fill(matrices, getThumbX() - getThumbWidth() / 2 + 1, sliderBounds.y() + 1, getThumbX() + getThumbWidth() / 2 + 1, sliderBounds.yLimit() + 1, 0xFF404040); - // thumb - GuiComponent.fill(matrices, getThumbX() - getThumbWidth() / 2, sliderBounds.y(), getThumbX() + getThumbWidth() / 2, sliderBounds.yLimit(), -1); - } - - @Override - protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { - matrices.pushPose(); - if (isHovered()) - matrices.translate(-(sliderBounds.width() + 6 + getThumbWidth() / 2f), 0, 0); - super.drawValueText(matrices, mouseX, mouseY, delta); - matrices.popPose(); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (!isAvailable() || button != 0 || !sliderBounds.isPointInside((int) mouseX, (int) mouseY)) - return false; - - mouseDown = true; - - setValueFromMouse(mouseX); - return true; - } - - @Override - public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { - if (!isAvailable() || button != 0 || !mouseDown) - return false; - - setValueFromMouse(mouseX); - return true; - } - - public void incrementValue(double amount) { - control.setPendingValue(Mth.clamp(control.pendingValue() + interval * amount, min, max)); - calculateInterpolation(); - } - - @Override - public boolean mouseScrolled(double mouseX, double mouseY, double amount) { - if (!isAvailable() || (!isMouseOver(mouseX, mouseY)) || (!Screen.hasShiftDown() && !Screen.hasControlDown())) - return false; - - incrementValue(amount); - return true; - } - - @Override - public boolean mouseReleased(double mouseX, double mouseY, int button) { - if (isAvailable() && mouseDown) - playDownSound(); - mouseDown = false; - - return super.mouseReleased(mouseX, mouseY, button); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused) - return false; - - switch (keyCode) { - case InputConstants.KEY_LEFT -> incrementValue(-1); - case InputConstants.KEY_RIGHT -> incrementValue(1); - default -> { - return false; - } - } - - return true; - } - - @Override - public boolean isMouseOver(double mouseX, double mouseY) { - return super.isMouseOver(mouseX, mouseY) || mouseDown; - } - - protected void setValueFromMouse(double mouseX) { - double value = (mouseX - sliderBounds.x()) / sliderBounds.width() * control.range(); - control.setPendingValue(roundToInterval(value)); - calculateInterpolation(); - } - - protected double roundToInterval(double value) { - return Mth.clamp(min + (interval * Math.round(value / interval)), min, max); // extremely imprecise, requires clamping - } - - @Override - protected int getHoveredControlWidth() { - return sliderBounds.width() + getUnhoveredControlWidth() + 6 + getThumbWidth() / 2; - } - - protected void calculateInterpolation() { - interpolation = Mth.clamp((float) ((control.pendingValue() - control.min()) * 1 / control.range()), 0f, 1f); - } - - @Override - public void setDimension(Dimension dim) { - super.setDimension(dim); - int trackWidth = dim.width() / 3; - if (optionNameString.isEmpty()) - trackWidth = dim.width() / 2; - - sliderBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - getThumbWidth() / 2 - trackWidth, dim.centerY() - 5, trackWidth, 10); - } - - protected int getThumbX() { - return (int) (sliderBounds.x() + sliderBounds.width() * interpolation); - } - - protected int getThumbWidth() { - return 4; - } - - @Override - public void postRender(PoseStack matrices, int mouseX, int mouseY, float delta) { - if (super.isMouseOver(mouseX, mouseY) || focused) - super.postRender(matrices, mouseX, mouseY, delta); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/slider/package-info.java b/src/client/java/dev/isxander/yacl/gui/controllers/slider/package-info.java deleted file mode 100644 index bff0d57..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/slider/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This package contains implementations of sliders for different number types - *

    - *
  • For doubles: {@link dev.isxander.yacl.gui.controllers.slider.DoubleSliderController}
  • - *
  • For floats: {@link dev.isxander.yacl.gui.controllers.slider.FloatSliderController}
  • - *
  • For integers: {@link dev.isxander.yacl.gui.controllers.slider.IntegerSliderController}
  • - *
  • For longs: {@link dev.isxander.yacl.gui.controllers.slider.LongSliderController}
  • - *
- */ -package dev.isxander.yacl.gui.controllers.slider; diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/IStringController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/IStringController.java deleted file mode 100644 index 6a603d2..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/IStringController.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string; - -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.network.chat.Component; - -/** - * A controller that can be any type but can input and output a string. - */ -public interface IStringController extends Controller { - /** - * Gets the option's pending value as a string. - * - * @see Option#pendingValue() - */ - String getString(); - - /** - * Sets the option's pending value from a string. - * - * @see Option#requestSet(Object) - */ - void setFromString(String value); - - /** - * {@inheritDoc} - */ - @Override - default Component formatValue() { - return Component.literal(getString()); - } - - default boolean isInputValid(String input) { - return true; - } - - @Override - default AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new StringControllerElement(this, screen, widgetDimension, true); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/StringController.java deleted file mode 100644 index 3a07641..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringController.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; - -/** - * A custom text field implementation for strings. - */ -public class StringController implements IStringController { - private final Option option; - - /** - * Constructs a string controller - * - * @param option bound option - */ - public StringController(Option option) { - this.option = option; - } - - /** - * {@inheritDoc} - */ - @Override - public Option option() { - return option; - } - - @Override - public String getString() { - return option().pendingValue(); - } - - @Override - public void setFromString(String value) { - option().requestSet(value); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java deleted file mode 100644 index cb2ad4e..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java +++ /dev/null @@ -1,408 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string; - -import com.mojang.blaze3d.platform.InputConstants; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ControllerWidget; -import dev.isxander.yacl.gui.utils.GuiUtils; -import net.minecraft.ChatFormatting; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; - -import java.util.function.Consumer; - -public class StringControllerElement extends ControllerWidget> { - protected final boolean instantApply; - - protected String inputField; - protected Dimension inputFieldBounds; - protected boolean inputFieldFocused; - - protected int caretPos; - protected int selectionLength; - - protected int renderOffset; - - protected float ticks; - - private final Component emptyText; - - public StringControllerElement(IStringController control, YACLScreen screen, Dimension dim, boolean instantApply) { - super(control, screen, dim); - this.instantApply = instantApply; - inputField = control.getString(); - inputFieldFocused = false; - selectionLength = 0; - emptyText = Component.literal("Click to type...").withStyle(ChatFormatting.GRAY); - control.option().addListener((opt, val) -> inputField = control.getString()); - setDimension(dim); - } - - @Override - protected void drawHoveredControl(PoseStack matrices, int mouseX, int mouseY, float delta) { - - } - - @Override - protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) { - Component valueText = getValueText(); - if (!isHovered()) valueText = Component.literal(GuiUtils.shortenString(valueText.getString(), textRenderer, getMaxUnwrapLength(), "...")).setStyle(valueText.getStyle()); - - matrices.pushPose(); - int textX = getDimension().xLimit() - textRenderer.width(valueText) + renderOffset - getXPadding(); - matrices.translate(textX, getTextY(), 0); - GuiUtils.enableScissor(inputFieldBounds.x(), inputFieldBounds.y() - 2, inputFieldBounds.width() + 1, inputFieldBounds.height() + 4); - textRenderer.drawShadow(matrices, valueText, 0, 0, getValueColor()); - matrices.popPose(); - - if (isHovered()) { - ticks += delta; - - String text = getValueText().getString(); - - GuiComponent.fill(matrices, inputFieldBounds.x(), inputFieldBounds.yLimit(), inputFieldBounds.xLimit(), inputFieldBounds.yLimit() + 1, -1); - GuiComponent.fill(matrices, inputFieldBounds.x() + 1, inputFieldBounds.yLimit() + 1, inputFieldBounds.xLimit() + 1, inputFieldBounds.yLimit() + 2, 0xFF404040); - - if (inputFieldFocused || focused) { - if (caretPos > text.length()) - caretPos = text.length(); - - int caretX = textX + textRenderer.width(text.substring(0, caretPos)) - 1; - if (text.isEmpty()) - caretX = inputFieldBounds.x() + inputFieldBounds.width() / 2; - - if (ticks % 20 <= 10) { - GuiComponent.fill(matrices, caretX, inputFieldBounds.y(), caretX + 1, inputFieldBounds.yLimit(), -1); - } - - if (selectionLength != 0) { - int selectionX = textX + textRenderer.width(text.substring(0, caretPos + selectionLength)); - GuiComponent.fill(matrices, caretX, inputFieldBounds.y() - 1, selectionX, inputFieldBounds.yLimit(), 0x803030FF); - } - } - } - RenderSystem.disableScissor(); - } - - @Override - public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (isAvailable() && getDimension().isPointInside((int) mouseX, (int) mouseY)) { - inputFieldFocused = true; - - if (!inputFieldBounds.isPointInside((int) mouseX, (int) mouseY)) { - caretPos = getDefaultCaretPos(); - } else { - // gets the appropriate caret position for where you click - int textX = (int) mouseX - (inputFieldBounds.xLimit() - textRenderer.width(getValueText())); - int pos = -1; - int currentWidth = 0; - for (char ch : inputField.toCharArray()) { - pos++; - int charLength = textRenderer.width(String.valueOf(ch)); - if (currentWidth + charLength / 2 > textX) { // if more than halfway past the characters select in front of that char - caretPos = pos; - break; - } else if (pos == inputField.length() - 1) { - // if we have reached the end and no matches, it must be the second half of the char so the last position - caretPos = pos + 1; - } - currentWidth += charLength; - } - - selectionLength = 0; - } - return true; - } else { - inputFieldFocused = false; - } - - return false; - } - - protected int getDefaultCaretPos() { - return inputField.length(); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!inputFieldFocused) - return false; - - switch (keyCode) { - case InputConstants.KEY_ESCAPE, InputConstants.KEY_RETURN -> { - unfocus(); - return true; - } - case InputConstants.KEY_LEFT -> { - if (Screen.hasShiftDown()) { - if (Screen.hasControlDown()) { - int spaceChar = findSpaceIndex(true); - selectionLength += caretPos - spaceChar; - caretPos = spaceChar; - } else if (caretPos > 0) { - caretPos--; - selectionLength += 1; - } - checkRenderOffset(); - } else { - if (caretPos > 0) { - if (selectionLength != 0) - caretPos += Math.min(selectionLength, 0); - else - caretPos--; - } - checkRenderOffset(); - selectionLength = 0; - } - - return true; - } - case InputConstants.KEY_RIGHT -> { - if (Screen.hasShiftDown()) { - if (Screen.hasControlDown()) { - int spaceChar = findSpaceIndex(false); - selectionLength -= spaceChar - caretPos; - caretPos = spaceChar; - } else if (caretPos < inputField.length()) { - caretPos++; - selectionLength -= 1; - } - checkRenderOffset(); - } else { - if (caretPos < inputField.length()) { - if (selectionLength != 0) - caretPos += Math.max(selectionLength, 0); - else - caretPos++; - checkRenderOffset(); - } - selectionLength = 0; - } - - return true; - } - case InputConstants.KEY_BACKSPACE -> { - doBackspace(); - return true; - } - case InputConstants.KEY_DELETE -> { - doDelete(); - return true; - } - } - - if (Screen.isPaste(keyCode)) { - return doPaste(); - } else if (Screen.isCopy(keyCode)) { - return doCopy(); - } else if (Screen.isCut(keyCode)) { - return doCut(); - } else if (Screen.isSelectAll(keyCode)) { - return doSelectAll(); - } - - return false; - } - - protected boolean doPaste() { - this.write(client.keyboardHandler.getClipboard()); - return true; - } - - protected boolean doCopy() { - if (selectionLength != 0) { - client.keyboardHandler.setClipboard(getSelection()); - return true; - } - return false; - } - - protected boolean doCut() { - if (selectionLength != 0) { - client.keyboardHandler.setClipboard(getSelection()); - this.write(""); - return true; - } - return false; - } - - protected boolean doSelectAll() { - caretPos = inputField.length(); - checkRenderOffset(); - selectionLength = -caretPos; - return true; - } - - protected void checkRenderOffset() { - if (textRenderer.width(inputField) < getUnshiftedLength()) { - renderOffset = 0; - return; - } - - int textX = getDimension().xLimit() - textRenderer.width(inputField) - getXPadding(); - int caretX = textX + textRenderer.width(inputField.substring(0, caretPos)) - 1; - - int minX = getDimension().xLimit() - getXPadding() - getUnshiftedLength(); - int maxX = minX + getUnshiftedLength(); - - if (caretX + renderOffset < minX) { - renderOffset = minX - caretX; - } else if (caretX + renderOffset > maxX) { - renderOffset = maxX - caretX; - } - } - - @Override - public boolean charTyped(char chr, int modifiers) { - if (!inputFieldFocused) - return false; - - write(Character.toString(chr)); - - return true; - } - - protected void doBackspace() { - if (selectionLength != 0) { - write(""); - } else if (caretPos > 0) { - if (modifyInput(builder -> builder.deleteCharAt(caretPos - 1))) { - caretPos--; - checkRenderOffset(); - } - } - } - - protected void doDelete() { - if (selectionLength != 0) { - write(""); - } else if (caretPos < inputField.length()) { - modifyInput(builder -> builder.deleteCharAt(caretPos)); - } - } - - public void write(String string) { - if (selectionLength == 0) { - if (modifyInput(builder -> builder.insert(caretPos, string))) { - caretPos += string.length(); - checkRenderOffset(); - } - } else { - int start = getSelectionStart(); - int end = getSelectionEnd(); - - if (modifyInput(builder -> builder.replace(start, end, string))) { - caretPos = start + string.length(); - selectionLength = 0; - checkRenderOffset(); - } - } - } - - public boolean modifyInput(Consumer consumer) { - StringBuilder temp = new StringBuilder(inputField); - consumer.accept(temp); - if (!control.isInputValid(temp.toString())) - return false; - inputField = temp.toString(); - if (instantApply) - updateControl(); - return true; - } - - public int getUnshiftedLength() { - if (optionNameString.isEmpty()) - return getDimension().width() - getXPadding() * 2; - return getDimension().width() / 8 * 5; - } - - public int getMaxUnwrapLength() { - if (optionNameString.isEmpty()) - return getDimension().width() - getXPadding() * 2; - return getDimension().width() / 2; - } - - public int getSelectionStart() { - return Math.min(caretPos, caretPos + selectionLength); - } - - public int getSelectionEnd() { - return Math.max(caretPos, caretPos + selectionLength); - } - - protected String getSelection() { - return inputField.substring(getSelectionStart(), getSelectionEnd()); - } - - protected int findSpaceIndex(boolean reverse) { - int i; - int fromIndex = caretPos; - if (reverse) { - if (caretPos > 0) - fromIndex -= 1; - i = this.inputField.lastIndexOf(" ", fromIndex); - - if (i == -1) i = 0; - } else { - if (caretPos < inputField.length()) - fromIndex += 1; - i = this.inputField.indexOf(" ", fromIndex); - - if (i == -1) i = inputField.length(); - } - - return i; - } - - @Override - public void setFocused(boolean focused) { - super.setFocused(focused); - inputFieldFocused = focused; - } - - @Override - public void unfocus() { - super.unfocus(); - inputFieldFocused = false; - renderOffset = 0; - if (!instantApply) updateControl(); - } - - @Override - public void setDimension(Dimension dim) { - super.setDimension(dim); - - int width = Math.max(6, Math.min(textRenderer.width(getValueText()), getUnshiftedLength())); - inputFieldBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - width, dim.centerY() - textRenderer.lineHeight / 2, width, textRenderer.lineHeight); - } - - @Override - public boolean isHovered() { - return super.isHovered() || inputFieldFocused; - } - - protected void updateControl() { - control.setFromString(inputField); - } - - @Override - protected int getUnhoveredControlWidth() { - return !isHovered() ? Math.min(getHoveredControlWidth(), getMaxUnwrapLength()) : getHoveredControlWidth(); - } - - @Override - protected int getHoveredControlWidth() { - return Math.min(textRenderer.width(getValueText()), getUnshiftedLength()); - } - - @Override - protected Component getValueText() { - if (!inputFieldFocused && inputField.isEmpty()) - return emptyText; - - return instantApply || !inputFieldFocused ? control.formatValue() : Component.literal(inputField); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java deleted file mode 100644 index df28241..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/DoubleFieldController.java +++ /dev/null @@ -1,104 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string.number; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.gui.controllers.slider.DoubleSliderController; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * {@inheritDoc} - */ -public class DoubleFieldController extends NumberFieldController { - private final double min, max; - - /** - * Constructs a double field controller - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - * @param formatter display text, not used whilst editing - */ - public DoubleFieldController(Option option, double min, double max, Function formatter) { - super(option, formatter); - this.min = min; - this.max = max; - } - - /** - * Constructs a double field controller. - * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - */ - public DoubleFieldController(Option option, double min, double max) { - this(option, min, max, DoubleSliderController.DEFAULT_FORMATTER); - } - - /** - * Constructs a double field controller. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - * @param formatter display text, not used whilst editing - */ - public DoubleFieldController(Option option, Function formatter) { - this(option, -Double.MAX_VALUE, Double.MAX_VALUE, formatter); - } - - /** - * Constructs a double field controller. - * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - */ - public DoubleFieldController(Option option) { - this(option, -Double.MAX_VALUE, Double.MAX_VALUE, DoubleSliderController.DEFAULT_FORMATTER); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return this.min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return this.max; - } - - /** - * {@inheritDoc} - */ - @Override - public String getString() { - return String.valueOf(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet(value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java deleted file mode 100644 index 957100a..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/FloatFieldController.java +++ /dev/null @@ -1,104 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string.number; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * {@inheritDoc} - */ -public class FloatFieldController extends NumberFieldController { - private final float min, max; - - /** - * Constructs a double field controller - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - * @param formatter display text, not used whilst editing - */ - public FloatFieldController(Option option, float min, float max, Function formatter) { - super(option, formatter); - this.min = min; - this.max = max; - } - - /** - * Constructs a double field controller. - * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - */ - public FloatFieldController(Option option, float min, float max) { - this(option, min, max, FloatSliderController.DEFAULT_FORMATTER); - } - - /** - * Constructs a double field controller. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - * @param formatter display text, not used whilst editing - */ - public FloatFieldController(Option option, Function formatter) { - this(option, -Float.MAX_VALUE, Float.MAX_VALUE, formatter); - } - - /** - * Constructs a double field controller. - * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - */ - public FloatFieldController(Option option) { - this(option, -Float.MAX_VALUE, Float.MAX_VALUE, FloatSliderController.DEFAULT_FORMATTER); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return this.min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return this.max; - } - - /** - * {@inheritDoc} - */ - @Override - public String getString() { - return String.valueOf(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((float) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java deleted file mode 100644 index 2d64a3a..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/IntegerFieldController.java +++ /dev/null @@ -1,109 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string.number; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * {@inheritDoc} - */ -public class IntegerFieldController extends NumberFieldController { - private final int min, max; - - /** - * Constructs a double field controller - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - * @param formatter display text, not used whilst editing - */ - public IntegerFieldController(Option option, int min, int max, Function formatter) { - super(option, formatter); - this.min = min; - this.max = max; - } - - /** - * Constructs a double field controller. - * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - */ - public IntegerFieldController(Option option, int min, int max) { - this(option, min, max, IntegerSliderController.DEFAULT_FORMATTER); - } - - /** - * Constructs a double field controller. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - * @param formatter display text, not used whilst editing - */ - public IntegerFieldController(Option option, Function formatter) { - this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, formatter); - } - - /** - * Constructs a double field controller. - * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - */ - public IntegerFieldController(Option option) { - this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, IntegerSliderController.DEFAULT_FORMATTER); - } - - @Override - public boolean isInputValid(String input) { - return input.matches("\\d+|-|"); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return this.min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return this.max; - } - - /** - * {@inheritDoc} - */ - @Override - public String getString() { - return String.valueOf(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((int) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java deleted file mode 100644 index a640621..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/LongFieldController.java +++ /dev/null @@ -1,109 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string.number; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.gui.controllers.slider.LongSliderController; -import net.minecraft.network.chat.Component; - -import java.util.function.Function; - -/** - * {@inheritDoc} - */ -public class LongFieldController extends NumberFieldController { - private final long min, max; - - /** - * Constructs a double field controller - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - * @param formatter display text, not used whilst editing - */ - public LongFieldController(Option option, long min, long max, Function formatter) { - super(option, formatter); - this.min = min; - this.max = max; - } - - /** - * Constructs a double field controller. - * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * - * @param option option to bind controller to - * @param min minimum allowed value (clamped on apply) - * @param max maximum allowed value (clamped on apply) - */ - public LongFieldController(Option option, long min, long max) { - this(option, min, max, LongSliderController.DEFAULT_FORMATTER); - } - - /** - * Constructs a double field controller. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - * @param formatter display text, not used whilst editing - */ - public LongFieldController(Option option, Function formatter) { - this(option, -Long.MAX_VALUE, Long.MAX_VALUE, formatter); - } - - /** - * Constructs a double field controller. - * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, - * not used whilst editing. - * Does not have a minimum or a maximum range. - * - * @param option option to bind controller to - */ - public LongFieldController(Option option) { - this(option, -Long.MAX_VALUE, Long.MAX_VALUE, LongSliderController.DEFAULT_FORMATTER); - } - - @Override - public boolean isInputValid(String input) { - return input.matches("\\d+|-|"); - } - - /** - * {@inheritDoc} - */ - @Override - public double min() { - return this.min; - } - - /** - * {@inheritDoc} - */ - @Override - public double max() { - return this.max; - } - - /** - * {@inheritDoc} - */ - @Override - public String getString() { - return String.valueOf(option().pendingValue()); - } - - /** - * {@inheritDoc} - */ - @Override - public void setPendingValue(double value) { - option().requestSet((long) value); - } - - /** - * {@inheritDoc} - */ - @Override - public double pendingValue() { - return option().pendingValue(); - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java deleted file mode 100644 index 4240849..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/NumberFieldController.java +++ /dev/null @@ -1,69 +0,0 @@ -package dev.isxander.yacl.gui.controllers.string.number; - -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.slider.ISliderController; -import dev.isxander.yacl.gui.controllers.string.IStringController; -import dev.isxander.yacl.gui.controllers.string.StringControllerElement; -import net.minecraft.network.chat.Component; -import net.minecraft.util.Mth; - -import java.text.DecimalFormatSymbols; -import java.util.function.Function; - -/** - * Controller that allows you to enter in numbers using a text field. - * - * @param number type - */ -public abstract class NumberFieldController implements ISliderController, IStringController { - private final Option option; - private final Function displayFormatter; - - public NumberFieldController(Option option, Function displayFormatter) { - this.option = option; - this.displayFormatter = displayFormatter; - } - - @Override - public Option option() { - return this.option; - } - - @Override - public void setFromString(String value) { - if (value.isEmpty() || value.equals(".") || value.equals("-")) value = "0"; - setPendingValue(Mth.clamp(Double.parseDouble(cleanupNumberString(value)), min(), max())); - } - - @Override - public double pendingValue() { - return option().pendingValue().doubleValue(); - } - - @Override - public boolean isInputValid(String input) { - return input.matches("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)|[.]||-"); - } - - @Override - public Component formatValue() { - return displayFormatter.apply(option().pendingValue()); - } - - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new StringControllerElement(this, screen, widgetDimension, false); - } - - protected String cleanupNumberString(String number) { - return number.replace(String.valueOf(DecimalFormatSymbols.getInstance().getGroupingSeparator()), ""); - } - - @Override - public double interval() { - return -1; - } -} diff --git a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java b/src/client/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java deleted file mode 100644 index 86b9314..0000000 --- a/src/client/java/dev/isxander/yacl/gui/controllers/string/number/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * This package contains implementations of input fields for different number types - *
    - *
  • For doubles: {@link dev.isxander.yacl.gui.controllers.string.number.DoubleFieldController}
  • - *
  • For floats: {@link dev.isxander.yacl.gui.controllers.string.number.FloatFieldController}
  • - *
  • For integers: {@link dev.isxander.yacl.gui.controllers.string.number.IntegerFieldController}
  • - *
  • For longs: {@link dev.isxander.yacl.gui.controllers.string.number.LongFieldController}
  • - *
- */ -package dev.isxander.yacl.gui.controllers.string.number; diff --git a/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java b/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java deleted file mode 100644 index aa8bbaa..0000000 --- a/src/client/java/dev/isxander/yacl/gui/utils/GuiUtils.java +++ /dev/null @@ -1,41 +0,0 @@ -package dev.isxander.yacl.gui.utils; - -import com.mojang.blaze3d.platform.Window; -import com.mojang.blaze3d.systems.RenderSystem; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.locale.Language; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; - -public class GuiUtils { - public static MutableComponent translatableFallback(String key, Component fallback) { - if (Language.getInstance().has(key)) - return Component.translatable(key); - return fallback.copy(); - } - - public static void enableScissor(int x, int y, int width, int height) { - Window window = Minecraft.getInstance().getWindow(); - double d = window.getGuiScale(); - RenderSystem.enableScissor((int)(x * d), (int)((window.getGuiScaledHeight() - y - height) * d), (int)(width * d), (int)(height * d)); - } - - public static String shortenString(String string, Font font, int maxWidth, String suffix) { - if (string.isEmpty()) - return string; - - boolean firstIter = true; - while (font.width(string) > maxWidth) { - string = string.substring(0, Math.max(string.length() - 1 - (firstIter ? 1 : suffix.length() + 1), 0)).trim(); - string += suffix; - - if (string.equals(suffix)) - break; - - firstIter = false; - } - - return string; - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java deleted file mode 100644 index 33cb474..0000000 --- a/src/client/java/dev/isxander/yacl/impl/ButtonOptionImpl.java +++ /dev/null @@ -1,218 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -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.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; - -@ApiStatus.Internal -public final class ButtonOptionImpl implements ButtonOption { - private final Component name; - private final Component tooltip; - private final BiConsumer action; - private boolean available; - private final Controller> controller; - private final Binding> binding; - - public ButtonOptionImpl( - @NotNull Component name, - @Nullable Component tooltip, - @NotNull BiConsumer action, - boolean available, - @NotNull Function>> controlGetter - ) { - this.name = name; - this.tooltip = tooltip; - this.action = action; - this.available = available; - this.controller = controlGetter.apply(this); - this.binding = new EmptyBinderImpl(); - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @Override - public BiConsumer action() { - return action; - } - - @Override - public boolean available() { - return available; - } - - @Override - public void setAvailable(boolean available) { - this.available = available; - } - - @Override - public @NotNull Controller> controller() { - return controller; - } - - @Override - public @NotNull Binding> binding() { - return binding; - } - - @Override - public @NotNull Class> typeClass() { - throw new UnsupportedOperationException(); - } - - @Override - public @NotNull ImmutableSet flags() { - return ImmutableSet.of(); - } - - @Override - public boolean changed() { - return false; - } - - @Override - public @NotNull BiConsumer pendingValue() { - throw new UnsupportedOperationException(); - } - - @Override - public void requestSet(BiConsumer value) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean applyValue() { - return false; - } - - @Override - public void forgetPendingValue() { - - } - - @Override - public void requestSetDefault() { - - } - - @Override - public boolean isPendingValueDefault() { - throw new UnsupportedOperationException(); - } - - @Override - public void addListener(BiConsumer>, BiConsumer> changedListener) { - - } - - private static class EmptyBinderImpl implements Binding> { - @Override - public void setValue(BiConsumer value) { - - } - - @Override - public BiConsumer getValue() { - throw new UnsupportedOperationException(); - } - - @Override - public BiConsumer defaultValue() { - throw new UnsupportedOperationException(); - } - } - - @ApiStatus.Internal - public static final class BuilderImpl implements ButtonOption.Builder { - private Component name; - private final List tooltipLines = new ArrayList<>(); - private boolean available = true; - private Function>> controlGetter; - private BiConsumer action; - - @Override - public ButtonOption.Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` cannot be null"); - - this.name = name; - return this; - } - - @Override - public ButtonOption.Builder tooltip(@NotNull Component... tooltips) { - Validate.notNull(tooltips, "`tooltips` cannot be empty"); - - tooltipLines.addAll(List.of(tooltips)); - return this; - } - - @Override - public ButtonOption.Builder action(@NotNull BiConsumer action) { - Validate.notNull(action, "`action` cannot be null"); - - this.action = action; - return this; - } - - @Override - @Deprecated - public ButtonOption.Builder action(@NotNull Consumer action) { - Validate.notNull(action, "`action` cannot be null"); - - this.action = (screen, button) -> action.accept(screen); - return this; - } - - @Override - public ButtonOption.Builder available(boolean available) { - this.available = available; - return this; - } - - @Override - public ButtonOption.Builder controller(@NotNull Function>> control) { - Validate.notNull(control, "`control` cannot be null"); - - this.controlGetter = control; - return this; - } - - @Override - public ButtonOption build() { - Validate.notNull(name, "`name` must not be null when building `Option`"); - Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); - Validate.notNull(action, "`action` must not be null when building `Option`"); - - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Component line : tooltipLines) { - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(line); - } - - return new ButtonOptionImpl(name, concatenatedTooltip, action, available, controlGetter); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java b/src/client/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java deleted file mode 100644 index 2d39eb9..0000000 --- a/src/client/java/dev/isxander/yacl/impl/ConfigCategoryImpl.java +++ /dev/null @@ -1,137 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.api.ConfigCategory; -import dev.isxander.yacl.api.ListOption; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionGroup; -import dev.isxander.yacl.impl.utils.YACLConstants; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentContents; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@ApiStatus.Internal -public final class ConfigCategoryImpl implements ConfigCategory { - private final Component name; - private final ImmutableList groups; - private final Component tooltip; - - public ConfigCategoryImpl(Component name, ImmutableList groups, Component tooltip) { - this.name = name; - this.groups = groups; - this.tooltip = tooltip; - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public @NotNull ImmutableList groups() { - return groups; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @ApiStatus.Internal - public static final class BuilderImpl implements Builder { - private Component name; - - private final List> rootOptions = new ArrayList<>(); - private final List groups = new ArrayList<>(); - - private final List tooltipLines = new ArrayList<>(); - - @Override - public Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` cannot be null"); - - this.name = name; - return this; - } - - @Override - public Builder option(@NotNull Option option) { - Validate.notNull(option, "`option` must not be null"); - - if (option instanceof ListOption listOption) { - YACLConstants.LOGGER.warn("Adding list option as an option is not supported! Rerouting to group!"); - return group(listOption); - } - - this.rootOptions.add(option); - return this; - } - - @Override - public Builder options(@NotNull Collection> options) { - Validate.notNull(options, "`options` must not be null"); - - if (options.stream().anyMatch(ListOption.class::isInstance)) - throw new UnsupportedOperationException("List options must not be added as an option but a group!"); - - this.rootOptions.addAll(options); - return this; - } - - @Override - public Builder group(@NotNull OptionGroup group) { - Validate.notNull(group, "`group` must not be null"); - - this.groups.add(group); - return this; - } - - @Override - public Builder groups(@NotNull Collection groups) { - Validate.notEmpty(groups, "`groups` must not be empty"); - - this.groups.addAll(groups); - return this; - } - - @Override - public Builder tooltip(@NotNull Component... tooltips) { - Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); - - tooltipLines.addAll(List.of(tooltips)); - return this; - } - - @Override - public ConfigCategory build() { - Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); - - List combinedGroups = new ArrayList<>(); - combinedGroups.add(new OptionGroupImpl(Component.empty(), Component.empty(), ImmutableList.copyOf(rootOptions), false, true)); - combinedGroups.addAll(groups); - - Validate.notEmpty(combinedGroups, "at least one option must be added to build `ConfigCategory`"); - - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Component line : tooltipLines) { - if (line.getContents() == ComponentContents.EMPTY) - continue; - - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(line); - } - - return new ConfigCategoryImpl(name, ImmutableList.copyOf(combinedGroups), concatenatedTooltip); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/GenericBindingImpl.java b/src/client/java/dev/isxander/yacl/impl/GenericBindingImpl.java deleted file mode 100644 index 0d668c6..0000000 --- a/src/client/java/dev/isxander/yacl/impl/GenericBindingImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.isxander.yacl.impl; - -import dev.isxander.yacl.api.Binding; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -public final class GenericBindingImpl implements Binding { - private final T def; - private final Supplier getter; - private final Consumer setter; - - public GenericBindingImpl(T def, Supplier getter, Consumer setting) { - this.def = def; - this.getter = getter; - this.setter = setting; - } - - - @Override - public void setValue(T value) { - setter.accept(value); - } - - @Override - public T getValue() { - return getter.get(); - } - - @Override - public T defaultValue() { - return def; - } - -} diff --git a/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java deleted file mode 100644 index b6c3c09..0000000 --- a/src/client/java/dev/isxander/yacl/impl/LabelOptionImpl.java +++ /dev/null @@ -1,154 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.gui.controllers.LabelController; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.function.BiConsumer; - -@ApiStatus.Internal -public final class LabelOptionImpl implements LabelOption { - private final Component label; - private final Component name = Component.literal("Label Option"); - private final Component tooltip = Component.empty(); - private final LabelController labelController; - private final Binding binding; - - public LabelOptionImpl(Component label) { - this.label = label; - this.labelController = new LabelController(this); - this.binding = Binding.immutable(label); - } - - @Override - public @NotNull Component label() { - return label; - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @Override - public @NotNull Controller controller() { - return labelController; - } - - @Override - public @NotNull Binding binding() { - return binding; - } - - @Override - public boolean available() { - return true; - } - - @Override - public void setAvailable(boolean available) { - throw new UnsupportedOperationException("Label options cannot be disabled."); - } - - @Override - public @NotNull Class typeClass() { - return Component.class; - } - - @Override - public @NotNull ImmutableSet flags() { - return ImmutableSet.of(); - } - - @Override - public boolean changed() { - return false; - } - - @Override - public @NotNull Component pendingValue() { - return label; - } - - @Override - public void requestSet(Component value) { - - } - - @Override - public boolean applyValue() { - return false; - } - - @Override - public void forgetPendingValue() { - - } - - @Override - public void requestSetDefault() { - - } - - @Override - public boolean isPendingValueDefault() { - return true; - } - - @Override - public boolean canResetToDefault() { - return false; - } - - @Override - public void addListener(BiConsumer, Component> changedListener) { - - } - - @ApiStatus.Internal - public static final class BuilderImpl implements LabelOption.Builder { - private final List lines = new ArrayList<>(); - - @Override - public dev.isxander.yacl.api.LabelOption.Builder line(@NotNull Component line) { - Validate.notNull(line, "`line` must not be null"); - - this.lines.add(line); - return this; - } - - @Override - public dev.isxander.yacl.api.LabelOption.Builder lines(@NotNull Collection lines) { - this.lines.addAll(lines); - return this; - } - - @Override - public LabelOption build() { - MutableComponent text = Component.empty(); - Iterator iterator = lines.iterator(); - while (iterator.hasNext()) { - text.append(iterator.next()); - - if (iterator.hasNext()) - text.append("\n"); - } - - return new LabelOptionImpl(text); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java b/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java deleted file mode 100644 index c15efe6..0000000 --- a/src/client/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java +++ /dev/null @@ -1,149 +0,0 @@ -package dev.isxander.yacl.impl; - -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.AbstractWidget; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ListEntryWidget; -import net.minecraft.network.chat.Component; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.function.BiConsumer; -import java.util.function.Function; - -@ApiStatus.Internal -public final class ListOptionEntryImpl implements ListOptionEntry { - private final ListOptionImpl group; - - private T value; - - private final Binding binding; - private final Controller controller; - - ListOptionEntryImpl(ListOptionImpl group, T initialValue, @NotNull Function, Controller> controlGetter) { - this.group = group; - this.value = initialValue; - this.binding = new EntryBinding(); - this.controller = new EntryController<>(controlGetter.apply(this), this); - } - - @Override - public @NotNull Component name() { - return Component.empty(); - } - - @Override - public @NotNull Component tooltip() { - return Component.empty(); - } - - @Override - public @NotNull Controller controller() { - return controller; - } - - @Override - public @NotNull Binding binding() { - return binding; - } - - @Override - public boolean available() { - return parentGroup().available(); - } - - @Override - public void setAvailable(boolean available) { - - } - - @Override - public ListOption parentGroup() { - return group; - } - - @Override - public boolean changed() { - return false; - } - - @Override - public @NotNull T pendingValue() { - return value; - } - - @Override - public void requestSet(T value) { - binding.setValue(value); - } - - @Override - public boolean applyValue() { - return false; - } - - @Override - public void forgetPendingValue() { - - } - - @Override - public void requestSetDefault() { - - } - - @Override - public boolean isPendingValueDefault() { - return false; - } - - @Override - public boolean canResetToDefault() { - return false; - } - - @Override - public void addListener(BiConsumer, T> changedListener) { - - } - - /** - * Open in case mods need to find the real controller type. - */ - @ApiStatus.Internal - public record EntryController(Controller controller, ListOptionEntryImpl entry) implements Controller { - @Override - public Option option() { - return controller.option(); - } - - @Override - public Component formatValue() { - return controller.formatValue(); - } - - @Override - public AbstractWidget provideWidget(YACLScreen screen, Dimension widgetDimension) { - return new ListEntryWidget(screen, entry, controller.provideWidget(screen, widgetDimension)); - } - } - - private class EntryBinding implements Binding { - @Override - public void setValue(T newValue) { - value = newValue; - group.callListeners(); - } - - @Override - public T getValue() { - return value; - } - - @Override - public T defaultValue() { - throw new UnsupportedOperationException(); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java b/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java deleted file mode 100644 index 27e90ad..0000000 --- a/src/client/java/dev/isxander/yacl/impl/ListOptionImpl.java +++ /dev/null @@ -1,338 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.api.*; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -@ApiStatus.Internal -public final class ListOptionImpl implements ListOption { - private final Component name; - private final Component tooltip; - private final Binding> binding; - private final T initialValue; - private final List> entries; - private final boolean collapsed; - private boolean available; - private final Class typeClass; - private final ImmutableSet flags; - private final EntryFactory entryFactory; - private final List>, List>> listeners; - private final List refreshListeners; - - public ListOptionImpl(@NotNull Component name, @NotNull Component tooltip, @NotNull Binding> binding, @NotNull T initialValue, @NotNull Class typeClass, @NotNull Function, Controller> controllerFunction, ImmutableSet flags, boolean collapsed, boolean available, Collection>, List>> listeners) { - this.name = name; - this.tooltip = tooltip; - this.binding = binding; - this.initialValue = initialValue; - this.entryFactory = new EntryFactory(controllerFunction); - this.entries = createEntries(binding().getValue()); - this.collapsed = collapsed; - this.typeClass = typeClass; - this.flags = flags; - this.available = available; - this.listeners = new ArrayList<>(); - this.listeners.addAll(listeners); - this.refreshListeners = new ArrayList<>(); - callListeners(); - } - - @Override - public @NotNull Component name() { - return this.name; - } - - @Override - public @NotNull Component tooltip() { - return this.tooltip; - } - - @Override - public @NotNull ImmutableList> options() { - return ImmutableList.copyOf(entries); - } - - @Override - public @NotNull Controller> controller() { - throw new UnsupportedOperationException(); - } - - @Override - public @NotNull Binding> binding() { - return binding; - } - - @Override - public @NotNull Class> typeClass() { - throw new UnsupportedOperationException(); - } - - @Override - public @NotNull Class elementTypeClass() { - return typeClass; - } - - @Override - public boolean collapsed() { - return collapsed; - } - - @Override - public @NotNull ImmutableSet flags() { - return flags; - } - - @Override - public @NotNull ImmutableList pendingValue() { - return ImmutableList.copyOf(entries.stream().map(Option::pendingValue).toList()); - } - - @Override - public void insertEntry(int index, ListOptionEntry entry) { - entries.add(index, (ListOptionEntry) entry); - onRefresh(); - } - - @Override - public ListOptionEntry insertNewEntryToTop() { - ListOptionEntry newEntry = entryFactory.create(initialValue); - entries.add(0, newEntry); - onRefresh(); - return newEntry; - } - - @Override - public void removeEntry(ListOptionEntry entry) { - if (entries.remove(entry)) - onRefresh(); - } - - @Override - public int indexOf(ListOptionEntry entry) { - return entries.indexOf(entry); - } - - @Override - public void requestSet(List value) { - entries.clear(); - entries.addAll(createEntries(value)); - onRefresh(); - } - - @Override - public boolean changed() { - return !binding().getValue().equals(pendingValue()); - } - - @Override - public boolean applyValue() { - if (changed()) { - binding().setValue(pendingValue()); - return true; - } - return false; - } - - @Override - public void forgetPendingValue() { - requestSet(binding().getValue()); - } - - @Override - public void requestSetDefault() { - requestSet(binding().defaultValue()); - } - - @Override - public boolean isPendingValueDefault() { - return binding().defaultValue().equals(pendingValue()); - } - - @Override - public boolean available() { - return available; - } - - @Override - public void setAvailable(boolean available) { - this.available = available; - } - - @Override - public void addListener(BiConsumer>, List> changedListener) { - this.listeners.add(changedListener); - } - - @Override - public void addRefreshListener(Runnable changedListener) { - this.refreshListeners.add(changedListener); - } - - @Override - public boolean isRoot() { - return false; - } - - private List> createEntries(Collection values) { - return values.stream().map(entryFactory::create).collect(Collectors.toList()); - } - - void callListeners() { - List pendingValue = pendingValue(); - this.listeners.forEach(listener -> listener.accept(this, pendingValue)); - } - - private void onRefresh() { - refreshListeners.forEach(Runnable::run); - callListeners(); - } - - private class EntryFactory { - private final Function, Controller> controllerFunction; - - private EntryFactory(Function, Controller> controllerFunction) { - this.controllerFunction = controllerFunction; - } - - public ListOptionEntry create(T initialValue) { - return new ListOptionEntryImpl<>(ListOptionImpl.this, initialValue, controllerFunction); - } - } - - @ApiStatus.Internal - public static final class BuilderImpl implements ListOption.Builder { - private Component name = Component.empty(); - private final List tooltipLines = new ArrayList<>(); - private Function, Controller> controllerFunction; - private Binding> binding = null; - private final Set flags = new HashSet<>(); - private T initialValue; - private boolean collapsed = false; - private boolean available = true; - private final List>, List>> listeners = new ArrayList<>(); - private final Class typeClass; - - public BuilderImpl(Class typeClass) { - this.typeClass = typeClass; - } - - @Override - public ListOption.Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` must not be null"); - - this.name = name; - return this; - } - - @Override - public ListOption.Builder tooltip(@NotNull Component... tooltips) { - Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); - - tooltipLines.addAll(List.of(tooltips)); - return this; - } - - @Override - public ListOption.Builder initial(@NotNull T initialValue) { - Validate.notNull(initialValue, "`initialValue` cannot be empty"); - - this.initialValue = initialValue; - return this; - } - - @Override - public ListOption.Builder controller(@NotNull Function, Controller> control) { - Validate.notNull(control, "`control` cannot be null"); - - this.controllerFunction = control; - return this; - } - - @Override - public ListOption.Builder binding(@NotNull Binding> binding) { - Validate.notNull(binding, "`binding` cannot be null"); - - this.binding = binding; - return this; - } - - @Override - public ListOption.Builder binding(@NotNull List def, @NotNull Supplier<@NotNull List> getter, @NotNull Consumer<@NotNull List> setter) { - Validate.notNull(def, "`def` must not be null"); - Validate.notNull(getter, "`getter` must not be null"); - Validate.notNull(setter, "`setter` must not be null"); - - this.binding = Binding.generic(def, getter, setter); - return this; - } - - @Override - public ListOption.Builder available(boolean available) { - this.available = available; - return this; - } - - @Override - public ListOption.Builder flag(@NotNull OptionFlag... flag) { - Validate.notNull(flag, "`flag` must not be null"); - - this.flags.addAll(Arrays.asList(flag)); - return this; - } - - @Override - public ListOption.Builder flags(@NotNull Collection flags) { - Validate.notNull(flags, "`flags` must not be null"); - - this.flags.addAll(flags); - return this; - } - - @Override - public ListOption.Builder collapsed(boolean collapsible) { - this.collapsed = collapsible; - return this; - } - - @Override - public ListOption.Builder listener(@NotNull BiConsumer>, List> listener) { - this.listeners.add(listener); - return this; - } - - @Override - public ListOption.Builder listeners(@NotNull Collection>, List>> listeners) { - this.listeners.addAll(listeners); - return this; - } - - @Override - public ListOption build() { - Validate.notNull(controllerFunction, "`controller` must not be null"); - Validate.notNull(binding, "`binding` must not be null"); - Validate.notNull(initialValue, "`initialValue` must not be null"); - - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Component line : tooltipLines) { - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(line); - } - - return new ListOptionImpl<>(name, concatenatedTooltip, binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java b/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java deleted file mode 100644 index 68f53a0..0000000 --- a/src/client/java/dev/isxander/yacl/impl/OptionGroupImpl.java +++ /dev/null @@ -1,129 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.api.ListOption; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionGroup; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentContents; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@ApiStatus.Internal -public final class OptionGroupImpl implements OptionGroup { - private final @NotNull Component name; - private final @NotNull Component tooltip; - private final ImmutableList> options; - private final boolean collapsed; - private final boolean isRoot; - - public OptionGroupImpl(@NotNull Component name, @NotNull Component tooltip, ImmutableList> options, boolean collapsed, boolean isRoot) { - this.name = name; - this.tooltip = tooltip; - this.options = options; - this.collapsed = collapsed; - this.isRoot = isRoot; - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @Override - public @NotNull ImmutableList> options() { - return options; - } - - @Override - public boolean collapsed() { - return collapsed; - } - - @Override - public boolean isRoot() { - return isRoot; - } - - @ApiStatus.Internal - public static final class BuilderImpl implements OptionGroup.Builder { - private Component name = Component.empty(); - private final List tooltipLines = new ArrayList<>(); - private final List> options = new ArrayList<>(); - private boolean collapsed = false; - - @Override - public Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` must not be null"); - - this.name = name; - return this; - } - - @Override - public Builder tooltip(@NotNull Component... tooltips) { - Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); - - tooltipLines.addAll(List.of(tooltips)); - return this; - } - - @Override - public Builder option(@NotNull Option option) { - Validate.notNull(option, "`option` must not be null"); - - if (option instanceof ListOption) - throw new UnsupportedOperationException("List options must not be added as an option but a group!"); - - this.options.add(option); - return this; - } - - @Override - public Builder options(@NotNull Collection> options) { - Validate.notEmpty(options, "`options` must not be empty"); - - if (options.stream().anyMatch(ListOption.class::isInstance)) - throw new UnsupportedOperationException("List options must not be added as an option but a group!"); - - this.options.addAll(options); - return this; - } - - @Override - public Builder collapsed(boolean collapsible) { - this.collapsed = collapsible; - return this; - } - - @Override - public OptionGroup build() { - Validate.notEmpty(options, "`options` must not be empty to build `OptionGroup`"); - - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Component line : tooltipLines) { - if (line.getContents() == ComponentContents.EMPTY) - continue; - - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(line); - } - - return new OptionGroupImpl(name, concatenatedTooltip, ImmutableList.copyOf(options), collapsed, false); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/OptionImpl.java b/src/client/java/dev/isxander/yacl/impl/OptionImpl.java deleted file mode 100644 index 644abc9..0000000 --- a/src/client/java/dev/isxander/yacl/impl/OptionImpl.java +++ /dev/null @@ -1,304 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableSet; -import dev.isxander.yacl.api.Binding; -import dev.isxander.yacl.api.Controller; -import dev.isxander.yacl.api.Option; -import dev.isxander.yacl.api.OptionFlag; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.ComponentContents; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.*; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Stream; - -@ApiStatus.Internal -public final class OptionImpl implements Option { - private final Component name; - private Component tooltip; - private final Controller controller; - private final Binding binding; - private boolean available; - - private final ImmutableSet flags; - - private final Class typeClass; - - private T pendingValue; - - private final List, T>> listeners; - - public OptionImpl( - @NotNull Component name, - @Nullable Function tooltipGetter, - @NotNull Function, Controller> controlGetter, - @NotNull Binding binding, - boolean available, - ImmutableSet flags, - @NotNull Class typeClass, - @NotNull Collection, T>> listeners - ) { - this.name = name; - this.binding = binding; - this.available = available; - this.flags = flags; - this.typeClass = typeClass; - this.listeners = new ArrayList<>(listeners); - this.controller = controlGetter.apply(this); - - addListener((opt, pending) -> tooltip = tooltipGetter.apply(pending)); - requestSet(binding().getValue()); - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @Override - public @NotNull Controller controller() { - return controller; - } - - @Override - public @NotNull Binding binding() { - return binding; - } - - @Override - public boolean available() { - return available; - } - - @Override - public void setAvailable(boolean available) { - this.available = available; - } - - @Override - public @NotNull Class typeClass() { - return typeClass; - } - - @Override - public @NotNull ImmutableSet flags() { - return flags; - } - - @Override - public boolean changed() { - return !binding().getValue().equals(pendingValue); - } - - @Override - public @NotNull T pendingValue() { - return pendingValue; - } - - @Override - public void requestSet(T value) { - pendingValue = value; - listeners.forEach(listener -> listener.accept(this, pendingValue)); - } - - @Override - public boolean applyValue() { - if (changed()) { - binding().setValue(pendingValue); - return true; - } - return false; - } - - @Override - public void forgetPendingValue() { - requestSet(binding().getValue()); - } - - @Override - public void requestSetDefault() { - requestSet(binding().defaultValue()); - } - - @Override - public boolean isPendingValueDefault() { - return binding().defaultValue().equals(pendingValue()); - } - - @Override - public void addListener(BiConsumer, T> changedListener) { - this.listeners.add(changedListener); - } - - @ApiStatus.Internal - public static class BuilderImpl implements Option.Builder { - private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED); - - private final List> tooltipGetters = new ArrayList<>(); - - private Function, Controller> controlGetter; - - private Binding binding; - - private boolean available = true; - - private boolean instant = false; - - private final Set flags = new HashSet<>(); - - private final Class typeClass; - - private final List, T>> listeners = new ArrayList<>(); - - public BuilderImpl(Class typeClass) { - this.typeClass = typeClass; - } - - @Override - public Option.Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` cannot be null"); - - this.name = name; - return this; - } - - @Override - public Builder tooltip(@NotNull Function tooltipGetter) { - Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); - - this.tooltipGetters.add(tooltipGetter); - return this; - } - - @Override - @SafeVarargs - @Deprecated - public final Option.Builder tooltip(@NotNull Function... tooltipGetter) { - Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null"); - - this.tooltipGetters.addAll(List.of(tooltipGetter)); - return this; - } - - @Override - public Option.Builder tooltip(@NotNull Component... tooltips) { - var tooltipFunctions = Arrays.stream(tooltips) - .map(t -> (Function) opt -> t) - .toList(); - - this.tooltipGetters.addAll(tooltipFunctions); - return this; - } - - @Override - public Option.Builder controller(@NotNull Function, Controller> control) { - Validate.notNull(control, "`control` cannot be null"); - - this.controlGetter = control; - return this; - } - - @Override - public Option.Builder binding(@NotNull Binding binding) { - Validate.notNull(binding, "`binding` cannot be null"); - - this.binding = binding; - return this; - } - - @Override - public Option.Builder binding(@NotNull T def, @NotNull Supplier<@NotNull T> getter, @NotNull Consumer<@NotNull T> setter) { - Validate.notNull(def, "`def` must not be null"); - Validate.notNull(getter, "`getter` must not be null"); - Validate.notNull(setter, "`setter` must not be null"); - - this.binding = Binding.generic(def, getter, setter); - return this; - } - - @Override - public Option.Builder available(boolean available) { - this.available = available; - return this; - } - - @Override - public Option.Builder flag(@NotNull OptionFlag... flag) { - Validate.notNull(flag, "`flag` must not be null"); - - this.flags.addAll(Arrays.asList(flag)); - return this; - } - - @Override - public Option.Builder flags(@NotNull Collection flags) { - Validate.notNull(flags, "`flags` must not be null"); - - this.flags.addAll(flags); - return this; - } - - @Override - public Option.Builder instant(boolean instant) { - this.instant = instant; - return this; - } - - @Override - public Option.Builder listener(@NotNull BiConsumer, T> listener) { - this.listeners.add(listener); - return this; - } - - @Override - public Option.Builder listeners(@NotNull Collection, T>> listeners) { - this.listeners.addAll(listeners); - return this; - } - - @Override - public Option build() { - Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); - Validate.notNull(binding, "`binding` must not be null when building `Option`"); - Validate.isTrue(!instant || flags.isEmpty(), "instant application does not support option flags"); - - Function concatenatedTooltipGetter = value -> { - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Function line : tooltipGetters) { - Component lineComponent = line.apply(value); - - if (lineComponent.getContents() == ComponentContents.EMPTY) - continue; - - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(lineComponent); - } - - return concatenatedTooltip; - }; - - if (instant) { - listeners.add((opt, pendingValue) -> opt.applyValue()); - } - - return new OptionImpl<>(name, concatenatedTooltipGetter, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java b/src/client/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java deleted file mode 100644 index 0b77466..0000000 --- a/src/client/java/dev/isxander/yacl/impl/PlaceholderCategoryImpl.java +++ /dev/null @@ -1,99 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.api.OptionGroup; -import dev.isxander.yacl.api.PlaceholderCategory; -import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.MutableComponent; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiFunction; - -@ApiStatus.Internal -public final class PlaceholderCategoryImpl implements PlaceholderCategory { - private final Component name; - private final BiFunction screen; - private final Component tooltip; - - public PlaceholderCategoryImpl(Component name, BiFunction screen, Component tooltip) { - this.name = name; - this.screen = screen; - this.tooltip = tooltip; - } - - @Override - public @NotNull ImmutableList groups() { - return ImmutableList.of(); - } - - @Override - public @NotNull Component name() { - return name; - } - - @Override - public BiFunction screen() { - return screen; - } - - @Override - public @NotNull Component tooltip() { - return tooltip; - } - - @ApiStatus.Internal - public static final class BuilderImpl implements PlaceholderCategory.Builder { - private Component name; - - private final List tooltipLines = new ArrayList<>(); - - private BiFunction screenFunction; - - @Override - public Builder name(@NotNull Component name) { - Validate.notNull(name, "`name` cannot be null"); - - this.name = name; - return this; - } - - @Override - public Builder tooltip(@NotNull Component... tooltips) { - Validate.notEmpty(tooltips, "`tooltips` cannot be empty"); - - tooltipLines.addAll(List.of(tooltips)); - return this; - } - - @Override - public Builder screen(@NotNull BiFunction screenFunction) { - Validate.notNull(screenFunction, "`screenFunction` cannot be null"); - - this.screenFunction = screenFunction; - return this; - } - - @Override - public PlaceholderCategory build() { - Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); - - MutableComponent concatenatedTooltip = Component.empty(); - boolean first = true; - for (Component line : tooltipLines) { - if (!first) concatenatedTooltip.append("\n"); - first = false; - - concatenatedTooltip.append(line); - } - - return new PlaceholderCategoryImpl(name, screenFunction, concatenatedTooltip); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java b/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java deleted file mode 100644 index 21c776a..0000000 --- a/src/client/java/dev/isxander/yacl/impl/YetAnotherConfigLibImpl.java +++ /dev/null @@ -1,122 +0,0 @@ -package dev.isxander.yacl.impl; - -import com.google.common.collect.ImmutableList; -import dev.isxander.yacl.api.ConfigCategory; -import dev.isxander.yacl.api.PlaceholderCategory; -import dev.isxander.yacl.api.YetAnotherConfigLib; -import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.impl.utils.YACLConstants; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.Component; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; - -@ApiStatus.Internal -public final class YetAnotherConfigLibImpl implements YetAnotherConfigLib { - private final Component title; - private final ImmutableList categories; - private final Runnable saveFunction; - private final Consumer initConsumer; - - private boolean generated = false; - - public YetAnotherConfigLibImpl(Component title, ImmutableList categories, Runnable saveFunction, Consumer initConsumer) { - this.title = title; - this.categories = categories; - this.saveFunction = saveFunction; - this.initConsumer = initConsumer; - } - - @Override - public Screen generateScreen(Screen parent) { - if (generated) - throw new UnsupportedOperationException("To prevent memory leaks, you should only generate a Screen once per instance. Please re-build the instance to generate another GUI."); - - YACLConstants.LOGGER.info("Generating YACL screen"); - generated = true; - return new YACLScreen(this, parent); - } - - @Override - public Component title() { - return title; - } - - @Override - public ImmutableList categories() { - return categories; - } - - @Override - public Runnable saveFunction() { - return saveFunction; - } - - @Override - public Consumer initConsumer() { - return initConsumer; - } - - @ApiStatus.Internal - public static final class BuilderImpl implements YetAnotherConfigLib.Builder { - private Component title; - private final List categories = new ArrayList<>(); - private Runnable saveFunction = () -> {}; - private Consumer initConsumer = screen -> {}; - - @Override - public YetAnotherConfigLib.Builder title(@NotNull Component title) { - Validate.notNull(title, "`title` cannot be null"); - - this.title = title; - return this; - } - - @Override - public YetAnotherConfigLib.Builder category(@NotNull ConfigCategory category) { - Validate.notNull(category, "`category` cannot be null"); - - this.categories.add(category); - return this; - } - - @Override - public YetAnotherConfigLib.Builder categories(@NotNull Collection categories) { - Validate.notNull(categories, "`categories` cannot be null"); - - this.categories.addAll(categories); - return this; - } - - @Override - public YetAnotherConfigLib.Builder save(@NotNull Runnable saveFunction) { - Validate.notNull(saveFunction, "`saveFunction` cannot be null"); - - this.saveFunction = saveFunction; - return this; - } - - @Override - public YetAnotherConfigLib.Builder screenInit(@NotNull Consumer initConsumer) { - Validate.notNull(initConsumer, "`initConsumer` cannot be null"); - - this.initConsumer = initConsumer; - return this; - } - - @Override - public YetAnotherConfigLib build() { - Validate.notNull(title, "`title must not be null to build `YetAnotherConfigLib`"); - Validate.notEmpty(categories, "`categories` must not be empty to build `YetAnotherConfigLib`"); - Validate.isTrue(!categories.stream().allMatch(category -> category instanceof PlaceholderCategory), "At least one regular category is required to build `YetAnotherConfigLib`"); - - return new YetAnotherConfigLibImpl(title, ImmutableList.copyOf(categories), saveFunction, initConsumer); - } - } -} diff --git a/src/client/java/dev/isxander/yacl/mixin/client/AbstractSelectionListMixin.java b/src/client/java/dev/isxander/yacl/mixin/client/AbstractSelectionListMixin.java deleted file mode 100644 index 978fd16..0000000 --- a/src/client/java/dev/isxander/yacl/mixin/client/AbstractSelectionListMixin.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.isxander.yacl.mixin.client; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.AbstractSelectionList; -import org.objectweb.asm.Opcodes; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import java.util.List; - -@Mixin(AbstractSelectionList.class) -public abstract class AbstractSelectionListMixin> { - @Shadow public abstract List children(); - - /** - * Mojang use the field access of children to get max index to loop through keyboard navigation to find the next entry. - * YACL modifies these children() method to filter out hidden entries, so we need to redirect the field access to the - * method, so we don't get ArrayIndexOutOfBoundsException. - */ - @Redirect(method = "nextEntry(Lnet/minecraft/client/gui/navigation/ScreenDirection;Ljava/util/function/Predicate;Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry;)Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry;", at = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/components/AbstractSelectionList;children:Ljava/util/List;", opcode = Opcodes.GETFIELD)) - private List modifyChildrenCall(AbstractSelectionList instance) { - return children(); - } -} diff --git a/src/client/java/dev/isxander/yacl/mixin/client/OptionInstanceAccessor.java b/src/client/java/dev/isxander/yacl/mixin/client/OptionInstanceAccessor.java deleted file mode 100644 index b8df39f..0000000 --- a/src/client/java/dev/isxander/yacl/mixin/client/OptionInstanceAccessor.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.isxander.yacl.mixin.client; - -import net.minecraft.client.OptionInstance; -import org.jetbrains.annotations.ApiStatus; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -@ApiStatus.Internal -@Mixin(OptionInstance.class) -public interface OptionInstanceAccessor { - @Accessor - T getInitialValue(); -} diff --git a/src/client/resources/yet-another-config-lib.client.mixins.json b/src/client/resources/yet-another-config-lib.client.mixins.json deleted file mode 100644 index 69c0f25..0000000 --- a/src/client/resources/yet-another-config-lib.client.mixins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "required": true, - "package": "dev.isxander.yacl.mixin.client", - "compatibilityLevel": "JAVA_17", - "injectors": { - "defaultRequire": 1 - }, - "client": [ - "OptionInstanceAccessor" - ], - "mixins": [ - "AbstractSelectionListMixin" - ] -} diff --git a/src/main/java/dev/isxander/yacl/api/utils/Dimension.java b/src/main/java/dev/isxander/yacl/api/utils/Dimension.java deleted file mode 100644 index 0de0a58..0000000 --- a/src/main/java/dev/isxander/yacl/api/utils/Dimension.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.isxander.yacl.api.utils; - -import dev.isxander.yacl.impl.utils.DimensionIntegerImpl; - -public interface Dimension { - T x(); - T y(); - - T width(); - T height(); - - T xLimit(); - T yLimit(); - - T centerX(); - T centerY(); - - boolean isPointInside(T x, T y); - - MutableDimension clone(); - - Dimension withX(T x); - Dimension withY(T y); - Dimension withWidth(T width); - Dimension withHeight(T height); - - Dimension moved(T x, T y); - Dimension expanded(T width, T height); - - static MutableDimension ofInt(int x, int y, int width, int height) { - return new DimensionIntegerImpl(x, y, width, height); - } -} diff --git a/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java b/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java deleted file mode 100644 index eff0186..0000000 --- a/src/main/java/dev/isxander/yacl/api/utils/MutableDimension.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.isxander.yacl.api.utils; - -public interface MutableDimension extends Dimension { - MutableDimension setX(T x); - MutableDimension setY(T y); - MutableDimension setWidth(T width); - MutableDimension setHeight(T height); - - MutableDimension move(T x, T y); - MutableDimension expand(T width, T height); -} diff --git a/src/main/java/dev/isxander/yacl/config/ConfigEntry.java b/src/main/java/dev/isxander/yacl/config/ConfigEntry.java deleted file mode 100644 index 7f04c33..0000000 --- a/src/main/java/dev/isxander/yacl/config/ConfigEntry.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.isxander.yacl.config; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface ConfigEntry { -} diff --git a/src/main/java/dev/isxander/yacl/config/ConfigInstance.java b/src/main/java/dev/isxander/yacl/config/ConfigInstance.java deleted file mode 100644 index c207161..0000000 --- a/src/main/java/dev/isxander/yacl/config/ConfigInstance.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.isxander.yacl.config; - -import java.lang.reflect.InvocationTargetException; - -/** - * Responsible for handing the actual config data type. - * Holds the instance along with a final default instance - * to reference default values for options and should not be changed. - * - * Abstract methods to save and load the class, implementations are responsible for - * how it saves and load. - * - * @param config data type - */ -public abstract class ConfigInstance { - private final Class configClass; - private final T defaultInstance; - private T instance; - - public ConfigInstance(Class configClass) { - this.configClass = configClass; - - try { - this.defaultInstance = this.instance = configClass.getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { - throw new IllegalStateException(String.format("Could not create default instance of config for %s. Make sure there is a default constructor!", this.configClass.getSimpleName())); - } - } - - public abstract void save(); - public abstract void load(); - - public T getConfig() { - return this.instance; - } - - protected void setConfig(T instance) { - this.instance = instance; - } - - public T getDefaults() { - return this.defaultInstance; - } - - public Class getConfigClass() { - return this.configClass; - } -} diff --git a/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java b/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java deleted file mode 100644 index ad7f550..0000000 --- a/src/main/java/dev/isxander/yacl/config/GsonConfigInstance.java +++ /dev/null @@ -1,212 +0,0 @@ -package dev.isxander.yacl.config; - -import com.google.gson.*; -import dev.isxander.yacl.impl.utils.YACLConstants; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; - -import java.awt.*; -import java.io.IOException; -import java.lang.reflect.Type; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.function.UnaryOperator; - -/** - * Uses GSON to serialize and deserialize config data from JSON to a file. - * - * Only fields annotated with {@link ConfigEntry} are included in the JSON. - * {@link Component}, {@link Style} and {@link Color} have default type adapters, so there is no need to provide them in your GSON instance. - * GSON is automatically configured to format fields as {@code lower_camel_case}. - * - * @param config data type - */ -public class GsonConfigInstance extends ConfigInstance { - private final Gson gson; - private final Path path; - - @Deprecated - public GsonConfigInstance(Class configClass, Path path) { - this(configClass, path, new GsonBuilder()); - } - - @Deprecated - public GsonConfigInstance(Class configClass, Path path, Gson gson) { - this(configClass, path, gson.newBuilder()); - } - - @Deprecated - public GsonConfigInstance(Class configClass, Path path, UnaryOperator builder) { - this(configClass, path, builder.apply(new GsonBuilder())); - } - - @Deprecated - public GsonConfigInstance(Class configClass, Path path, GsonBuilder builder) { - super(configClass); - this.path = path; - this.gson = builder - .setExclusionStrategies(new ConfigExclusionStrategy()) - .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) - .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) - .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()) - .serializeNulls() - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .create(); - } - - private GsonConfigInstance(Class configClass, Path path, Gson gson, boolean fromBuilder) { - super(configClass); - this.path = path; - this.gson = gson; - } - - @Override - public void save() { - try { - YACLConstants.LOGGER.info("Saving {}...", getConfigClass().getSimpleName()); - Files.writeString(path, gson.toJson(getConfig()), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void load() { - try { - if (Files.notExists(path)) { - save(); - return; - } - - YACLConstants.LOGGER.info("Loading {}...", getConfigClass().getSimpleName()); - setConfig(gson.fromJson(Files.readString(path), getConfigClass())); - } catch (IOException e) { - e.printStackTrace(); - } - } - - public Path getPath() { - return this.path; - } - - private static class ConfigExclusionStrategy implements ExclusionStrategy { - @Override - public boolean shouldSkipField(FieldAttributes fieldAttributes) { - return fieldAttributes.getAnnotation(ConfigEntry.class) == null; - } - - @Override - public boolean shouldSkipClass(Class aClass) { - return false; - } - } - - public static class ColorTypeAdapter implements JsonSerializer, JsonDeserializer { - @Override - public Color deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException { - return new Color(jsonElement.getAsInt(), true); - } - - @Override - public JsonElement serialize(Color color, Type type, JsonSerializationContext jsonSerializationContext) { - return new JsonPrimitive(color.getRGB()); - } - } - - /** - * Creates a builder for a GSON config instance. - * @param configClass the config class - * @return a new builder - * @param the config type - */ - public static Builder createBuilder(Class configClass) { - return new Builder<>(configClass); - } - - public static class Builder { - private final Class configClass; - private Path path; - private UnaryOperator gsonBuilder = builder -> builder - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .serializeNulls() - .registerTypeHierarchyAdapter(Component.class, new Component.Serializer()) - .registerTypeHierarchyAdapter(Style.class, new Style.Serializer()) - .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter()); - - private Builder(Class configClass) { - this.configClass = configClass; - } - - /** - * Sets the file path to save and load the config from. - */ - public Builder setPath(Path path) { - this.path = path; - return this; - } - - /** - * Sets the GSON instance to use. Overrides all YACL defaults such as: - *
    - *
  • lower_camel_case field naming policy
  • - *
  • null serialization
  • - *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • - *
- * Still respects the exclusion strategy to only serialize {@link ConfigEntry} - * but these can be added to with setExclusionStrategies. - * - * @param gsonBuilder gson builder to use - */ - public Builder overrideGsonBuilder(GsonBuilder gsonBuilder) { - this.gsonBuilder = builder -> gsonBuilder; - return this; - } - - /** - * Sets the GSON instance to use. Overrides all YACL defaults such as: - *
    - *
  • lower_camel_case field naming policy
  • - *
  • null serialization
  • - *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • - *
- * Still respects the exclusion strategy to only serialize {@link ConfigEntry} - * but these can be added to with setExclusionStrategies. - * - * @param gson gson instance to be converted to a builder - */ - public Builder overrideGsonBuilder(Gson gson) { - return this.overrideGsonBuilder(gson.newBuilder()); - } - - /** - * Appends extra configuration to a GSON builder. - * This is the intended way to add functionality to the GSON instance. - *

- * By default, YACL sets the GSON with the following options: - *

    - *
  • lower_camel_case field naming policy
  • - *
  • null serialization
  • - *
  • {@link Component}, {@link Style} and {@link Color} type adapters
  • - *
- * - * @param gsonBuilder the function to apply to the builder - */ - public Builder appendGsonBuilder(UnaryOperator gsonBuilder) { - this.gsonBuilder = builder -> gsonBuilder.apply(this.gsonBuilder.apply(builder)); - return this; - } - - /** - * Builds the config instance. - * @return the built config instance - */ - public GsonConfigInstance build() { - UnaryOperator gsonBuilder = builder -> this.gsonBuilder.apply(builder) - .addSerializationExclusionStrategy(new ConfigExclusionStrategy()) - .addDeserializationExclusionStrategy(new ConfigExclusionStrategy()); - - return new GsonConfigInstance<>(configClass, path, gsonBuilder.apply(new GsonBuilder()).create(), true); - } - } -} diff --git a/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java b/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java deleted file mode 100644 index 6c7508d..0000000 --- a/src/main/java/dev/isxander/yacl/impl/utils/DimensionIntegerImpl.java +++ /dev/null @@ -1,115 +0,0 @@ -package dev.isxander.yacl.impl.utils; - -import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.api.utils.MutableDimension; - -public class DimensionIntegerImpl implements MutableDimension { - private int x, y; - private int width, height; - - public DimensionIntegerImpl(int x, int y, int width, int height) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - - @Override - public Integer x() { - return x; - } - - @Override - public Integer y() { - return y; - } - - @Override - public Integer width() { - return width; - } - - @Override - public Integer height() { - return height; - } - - @Override - public Integer xLimit() { - return x + width; - } - - @Override - public Integer yLimit() { - return y + height; - } - - @Override - public Integer centerX() { - return x + width / 2; - } - - @Override - public Integer centerY() { - return y + height / 2; - } - - @Override - public boolean isPointInside(Integer x, Integer y) { - return x >= x() && x <= xLimit() && y >= y() && y <= yLimit(); - } - - @Override - public MutableDimension clone() { - return new DimensionIntegerImpl(x, y, width, height); - } - - @Override public MutableDimension setX(Integer x) { this.x = x; return this; } - @Override public MutableDimension setY(Integer y) { this.y = y; return this; } - @Override public MutableDimension setWidth(Integer width) { this.width = width; return this; } - @Override public MutableDimension setHeight(Integer height) { this.height = height; return this; } - - @Override - public Dimension withX(Integer x) { - return clone().setX(x); - } - - @Override - public Dimension withY(Integer y) { - return clone().setY(y); - } - - @Override - public Dimension withWidth(Integer width) { - return clone().setWidth(width); - } - - @Override - public Dimension withHeight(Integer height) { - return clone().setHeight(height); - } - - @Override - public MutableDimension move(Integer x, Integer y) { - this.x += x; - this.y += y; - return this; - } - - @Override - public MutableDimension expand(Integer width, Integer height) { - this.width += width; - this.height += height; - return this; - } - - @Override - public Dimension moved(Integer x, Integer y) { - return clone().move(x, y); - } - - @Override - public Dimension expanded(Integer width, Integer height) { - return clone().expand(width, height); - } -} diff --git a/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java b/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java deleted file mode 100644 index 3d382d4..0000000 --- a/src/main/java/dev/isxander/yacl/impl/utils/YACLConstants.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.isxander.yacl.impl.utils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class YACLConstants { - public static final Logger LOGGER = LoggerFactory.getLogger("YetAnotherConfigLib"); -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/el_gr.json b/src/main/resources/assets/yet-another-config-lib/lang/el_gr.json deleted file mode 100644 index b7bc2d2..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/el_gr.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "yacl.control.boolean.true": "Ναι", - "yacl.control.boolean.false": "Όχι", - - "yacl.control.action.execute": "ΕΚΤΕΛΕΣΗ", - - "yacl.gui.save": "Αποθήκευση Αλλαγών", - "yacl.gui.save.tooltip": "Μονιμοποιήει τις αλλαγές.", - "yacl.gui.finished.tooltip": "Κλείνει το GUI.", - "yacl.gui.cancel.tooltip": "Ξεχνά τις ενεργές αλλαγές και κλείνει το μενού.", - "yacl.gui.reset.tooltip": "Επαναφέρει όλες τις επιλογές στις προεπιλογές τους. (Μπορεί να ανερεθε)", - "yacl.gui.undo": "Επαναφορά", - "yacl.gui.undo.tooltip": "Επαναφέρει όλες τις ενεργές επιλογές στις προεπιλογές τους.", - "yacl.gui.fail_apply": "Αποτυχία εφαρμογής", - "yacl.gui.fail_apply.tooltip": "Δημιουργήθηκε ένα σφάλμα και οι αλλαγές δεν μπόρεσαν να εφαρμοστούν.", - "yacl.gui.save_before_exit": "Αποθήκευση πριν το κλείσιμο!", - "yacl.gui.save_before_exit.tooltip": "Αποθήκευσε ή ακύρωσε για να βγεις απ' το μενού." - - "yacl.restart.title": "Η ρύθμιση απαιτεί επανεκκήνιση!", - "yacl.restart.message": "Μία ή παραπάνω επιλογές προϋποθέτουν επανεκκήνηση το παιχνιδιού για να εφαρμοστούν.", - "yacl.restart.yes": "Κλείσιμο του Minecraft", - "yacl.restart.no": "Αγνόησε το για τώρα" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json b/src/main/resources/assets/yet-another-config-lib/lang/en_us.json deleted file mode 100644 index 32621e9..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/en_us.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "yacl.control.boolean.true": "True", - "yacl.control.boolean.false": "False", - - "yacl.control.action.execute": "EXECUTE", - - "yacl.gui.save": "Save Changes", - "yacl.gui.save.tooltip": "Makes the changes made permanent.", - "yacl.gui.finished.tooltip": "Closes the GUI.", - "yacl.gui.cancel.tooltip": "Forgets pending changes and closes the GUI.", - "yacl.gui.reset.tooltip": "Resets all options to default. (This is reversible!)", - "yacl.gui.undo": "Undo", - "yacl.gui.undo.tooltip": "Reverts all options back to what they were before editing.", - "yacl.gui.fail_apply": "Failed to apply", - "yacl.gui.fail_apply.tooltip": "There was an error and the changes couldn't be applied.", - "yacl.gui.save_before_exit": "Save before exiting!", - "yacl.gui.save_before_exit.tooltip": "Save or cancel to exit the GUI.", - - "yacl.list.move_up": "Move up", - "yacl.list.move_down": "Move down", - "yacl.list.remove": "Remove", - "yacl.list.add_top": "New entry", - "yacl.list.empty": "List is empty", - - "yacl.restart.title": "Config requires restart!", - "yacl.restart.message": "One or more options needs you to restart the game to apply the changes.", - "yacl.restart.yes": "Close Minecraft", - "yacl.restart.no": "Ignore" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/et_ee.json b/src/main/resources/assets/yet-another-config-lib/lang/et_ee.json deleted file mode 100644 index 5f5274a..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/et_ee.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "yacl.control.boolean.true": "Tõene", - "yacl.control.boolean.false": "Väär", - - "yacl.control.action.execute": "KÄIVITA", - - "yacl.gui.save": "Salvesta muudatused", - "yacl.gui.save.tooltip": "Teeb tehtud muudatused püsivaks.", - "yacl.gui.finished.tooltip": "Sulgeb liidese.", - "yacl.gui.cancel.tooltip": "Unustab tehtud muudatused ja sulgeb liidese.", - "yacl.gui.reset.tooltip": "Lähtestab kõik valikud vaikeväärtustele. (Seda saab tagasi võtta!)", - "yacl.gui.undo": "Võta tagasi", - "yacl.gui.undo.tooltip": "Lähtestab kõik valikud muutmise-eelsetele väärtustele.", - "yacl.gui.fail_apply": "Rakendamine ebaõnnestus", - "yacl.gui.fail_apply.tooltip": "Esines viga ja muudatusi ei saadud rakendada.", - "yacl.gui.save_before_exit": "Salvesta enne väljumist!", - "yacl.gui.save_before_exit.tooltip": "Liidesest väljumiseks salvesta või loobu." -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/fr_fr.json b/src/main/resources/assets/yet-another-config-lib/lang/fr_fr.json deleted file mode 100644 index bc069cf..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/fr_fr.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "yacl.control.boolean.true": "Vrai", - "yacl.control.boolean.false": "Faux", - - "yacl.control.action.execute": "EXÉCUTION", - - "yacl.gui.save": "Sauvegarder les modifications", - "yacl.gui.save.tooltip": "Rend les changements effectués permanents.", - "yacl.gui.finished.tooltip": "Ferme la superposition.", - "yacl.gui.cancel.tooltip": "Oublie les changements en cours et ferme la superposition.", - "yacl.gui.reset.tooltip": "Réinitialise toutes les options par défaut. (Réversible !)", - "yacl.gui.undo": "Annuler", - "yacl.gui.undo.tooltip": "Rétablit toutes les options telles qu'elles étaient avant l'édition.", - "yacl.gui.fail_apply": "Échec de l'application", - "yacl.gui.fail_apply.tooltip": "Il y a eu une erreur et les changements n'ont pas pu être appliqués.", - "yacl.gui.save_before_exit": "Sauvegardez avant de quitter !", - "yacl.gui.save_before_exit.tooltip": "Sauvegardez ou annulez pour quitter la superposition.", - - "yacl.list.move_up": "Monter en haut", - "yacl.list.move_down": "Monter en bas", - "yacl.list.remove": "Retirer", - "yacl.list.add_top": "Nouvelle entrée", - "yacl.list.empty": "La liste est vide", - - "yacl.restart.title": "La configuration nécessite un redémarrage !", - "yacl.restart.message": "Une ou plusieurs options nécessitent que vous redémarriez le jeu pour appliquer les changements.", - "yacl.restart.yes": "Fermer Minecraft", - "yacl.restart.no": "Ignorer" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/pl_pl.json b/src/main/resources/assets/yet-another-config-lib/lang/pl_pl.json deleted file mode 100644 index 49074ea..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/pl_pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "yacl.control.boolean.true": "Tak", - "yacl.control.boolean.false": "Nie", - - "yacl.control.action.execute": "WYKONAJ", - - "yacl.gui.save": "Zapisz zmiany", - "yacl.gui.save.tooltip": "Sprawia, że wprowadzone zmiany są trwałe.", - "yacl.gui.finished.tooltip": "Zamyka GUI.", - "yacl.gui.cancel.tooltip": "Zapomina oczekujące zmiany i zamyka GUI.", - "yacl.gui.reset.tooltip": "Resetuje wszystkie opcje do wartości domyślnych. (To jest odwracalne!)", - "yacl.gui.undo": "Cofnij", - "yacl.gui.undo.tooltip": "Przywraca wszystkie opcje do stanu sprzed edycji.", - "yacl.gui.fail_apply": "Nie udało się zastosować", - "yacl.gui.fail_apply.tooltip": "Wystąpił błąd i nie udało się zastosować zmian.", - "yacl.gui.save_before_exit": "Zapisz przed wyjściem!", - "yacl.gui.save_before_exit.tooltip": "Zapisz lub anuluj, aby wyjść z GUI.", - - "yacl.restart.title": "Konfiguracja wymaga restartu!", - "yacl.restart.message": "Jedna lub więcej opcji wymaga ponownego uruchomienia gry, aby zastosować zmiany.", - "yacl.restart.yes": "Zamknij Minecrafta", - "yacl.restart.no": "Ignoruj" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/pt_br.json b/src/main/resources/assets/yet-another-config-lib/lang/pt_br.json deleted file mode 100644 index 9d4ef8d..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/pt_br.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "yacl.control.boolean.true": "Verdadeiro", - "yacl.control.boolean.false": "Falso", - - "yacl.control.action.execute": "EXECUTAR", - - "yacl.gui.save": "Salvar Mudanças", - "yacl.gui.save.tooltip": "Faz as mudanças serem permanentes.", - "yacl.gui.finished.tooltip": "Fecha o GUI.", - "yacl.gui.cancel.tooltip": "Esquece as mudanças pendentes e fecha o GUI.", - "yacl.gui.reset.tooltip": "Reinicia todas as opções para o valor padrão. (Isso é irreversível!)", - "yacl.gui.undo": "Desfazer", - "yacl.gui.undo.tooltip": "Reverte todas as opções como elas estavam antes de serem editadas.", - "yacl.gui.fail_apply": "Falha na aplicação", - "yacl.gui.fail_apply.tooltip": "Houve um erro e as mudanças não puderam ser aplicadas.", - "yacl.gui.save_before_exit": "Salve antes de sair!", - "yacl.gui.save_before_exit.tooltip": "Salve ou calcele para sair do GUI." -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/ru_ru.json b/src/main/resources/assets/yet-another-config-lib/lang/ru_ru.json deleted file mode 100644 index 5725d34..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/ru_ru.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "yacl.control.boolean.true": "§atrue", - "yacl.control.boolean.false": "§cfalse", - - "yacl.control.action.execute": "Выполнить", - - "yacl.gui.save": "Сохранить", - "yacl.gui.save.tooltip": "Сохранить изменения до следующего редактирования.", - "yacl.gui.finished.tooltip": "Закрыть меню.", - "yacl.gui.cancel": "Назад", - "yacl.gui.cancel.tooltip": "Отменить изменения и закрыть настройки.", - "yacl.gui.reset.tooltip": "Сбросить все настройки до значений по умолчанию (их можно восстановить).", - "yacl.gui.undo": "Отменить", - "yacl.gui.undo.tooltip": "Вернуть все настройки к состоянию, в котором они были до изменений.", - "yacl.gui.fail_apply": "Не удалось сохранить", - "yacl.gui.fail_apply.tooltip": "Возникла ошибка; изменения невозможно применить.", - "yacl.gui.save_before_exit": "Сохраните перед закрытием", - "yacl.gui.save_before_exit.tooltip": "Сохраните или отмените изменения, чтобы закрыть настройки.", - - "yacl.restart.title": "Настройки требуют перезагрузки.", - "yacl.restart.message": "Одна или несколько настроек требует перезапуска игры для применения изменений.", - "yacl.restart.yes": "Закрыть Minecraft", - "yacl.restart.no": "Игнорировать" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/sl_si.json b/src/main/resources/assets/yet-another-config-lib/lang/sl_si.json deleted file mode 100644 index 743dd4d..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/sl_si.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "yacl.control.boolean.true": "Vklopljeno", - "yacl.control.boolean.false": "Izklopljeno", - - "yacl.control.action.execute": "POŽENI", - - "yacl.gui.save": "Shrani spremembe", - "yacl.gui.save.tooltip": "Uvede trajne spremembe.", - "yacl.gui.finished.tooltip": "Zapre meni.", - "yacl.gui.cancel.tooltip": "Zavrže neshranjene spremembe in zapre meni.", - "yacl.gui.reset.tooltip": "Ponastavi vse možnosti na privzete. (To se da razveljaviti!)", - "yacl.gui.undo": "Razveljavi", - "yacl.gui.undo.tooltip": "Ponastavi vse možnosti na take kot pred spreminjanjem.", - "yacl.gui.fail_apply": "Napaka pri uveljavljanju", - "yacl.gui.fail_apply.tooltip": "Prišlo je do napake pri uveljavljanju sprememb.", - "yacl.gui.save_before_exit": "Shrani pred izhodom!", - "yacl.gui.save_before_exit.tooltip": "Shrani ali prekliči za izhod iz menija.", - "yacl.restart.title": "Nastavitve potrebujejo ponovni zagon!", - "yacl.restart.message": "Vsaj ena izmed nastavitev potrebuje ponovni zagon igre za uveljavitev.", - "yacl.restart.yes": "Zapri Minecraft", - "yacl.restart.no": "Prezri" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/zh_cn.json b/src/main/resources/assets/yet-another-config-lib/lang/zh_cn.json deleted file mode 100644 index 9307c9b..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/zh_cn.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "yacl.control.boolean.true": "是", - "yacl.control.boolean.false": "否", - - "yacl.control.action.execute": "执行", - - "yacl.gui.save": "保存更改", - "yacl.gui.save.tooltip": "永久保存所做更改。", - "yacl.gui.finished.tooltip": "关闭 GUI。", - "yacl.gui.cancel.tooltip": "忽略犹豫不决的更改然后关闭 GUI。", - "yacl.gui.reset.tooltip": "将所有选项重置为默认值。(这是可逆的!)", - "yacl.gui.undo": "撤销", - "yacl.gui.undo.tooltip": "将所有选项恢复到编辑前的状态。", - "yacl.gui.fail_apply": "应用失败", - "yacl.gui.fail_apply.tooltip": "有一个错误以至于更改不能被应用。", - "yacl.gui.save_before_exit": "退出前保存!", - "yacl.gui.save_before_exit.tooltip": "保存或取消以退出 GUI。", - - "yacl.list.move_up": "上移", - "yacl.list.move_down": "下移", - "yacl.list.remove": "移除", - "yacl.list.add_top": "新条目", - "yacl.list.empty": "列表为空", - - "yacl.restart.title": "配置需要重新启动!", - "yacl.restart.message": "一个或多个选项需要你重新启动游戏来应用这些更改。", - "yacl.restart.yes": "关闭 Minecraft", - "yacl.restart.no": "忽略" -} diff --git a/src/main/resources/assets/yet-another-config-lib/lang/zh_tw.json b/src/main/resources/assets/yet-another-config-lib/lang/zh_tw.json deleted file mode 100644 index 67d05a7..0000000 --- a/src/main/resources/assets/yet-another-config-lib/lang/zh_tw.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "yacl.control.boolean.true": "是", - "yacl.control.boolean.false": "否", - - "yacl.control.action.execute": "執行", - - "yacl.gui.save": "儲存變更", - "yacl.gui.save.tooltip": "儲存你的變更。", - "yacl.gui.finished.tooltip": "關閉介面。", - "yacl.gui.cancel.tooltip": "取消變更並關閉介面。", - "yacl.gui.reset.tooltip": "重設所有選項到預設。(這可以復原!)", - "yacl.gui.undo": "復原", - "yacl.gui.undo.tooltip": "將所有選項恢復成編輯前的狀態。", - "yacl.gui.fail_apply": "套用失敗", - "yacl.gui.fail_apply.tooltip": "發生錯誤,無法套用變更。", - "yacl.gui.save_before_exit": "在離開時儲存!", - "yacl.gui.save_before_exit.tooltip": "儲存或是取消並離開介面。", - - "yacl.restart.title": "變更設定需要重開遊戲!", - "yacl.restart.message": "一個或多個選項需要你重開遊戲才能套用變更。", - "yacl.restart.yes": "關閉 Minecraft", - "yacl.restart.no": "忽略" -} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json deleted file mode 100644 index cc2e6b9..0000000 --- a/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "schemaVersion": 1, - "id": "${id}", - "version": "${version}", - "name": "${name}", - "description": "${description}", - "authors": [ - "isXander" - ], - "contact": { - "homepage": "https://isxander.dev", - "issues": "https://github.com/${github}/issues", - "sources": "https://github.com/${github}" - }, - "icon": "yacl-128x.png", - "license": "LGPL-3.0-or-later", - "environment": "*", - "entrypoints": { - - }, - "depends": { - "fabricloader": ">=0.14.0", - "minecraft": "~1.19.4", - "java": ">=17", - "fabric-resource-loader-v0": "*" - }, - "mixins": [ - { - "config": "yet-another-config-lib.client.mixins.json", - "environment": "client" - } - ], - "accessWidener": "yacl.accesswidener", - "custom": { - "modmenu": { - "badges": ["library"] - } - } -} diff --git a/src/main/resources/yacl-128x.png b/src/main/resources/yacl-128x.png deleted file mode 100644 index c86981c..0000000 Binary files a/src/main/resources/yacl-128x.png and /dev/null differ diff --git a/src/main/resources/yacl.accesswidener b/src/main/resources/yacl.accesswidener deleted file mode 100644 index 3a65a38..0000000 --- a/src/main/resources/yacl.accesswidener +++ /dev/null @@ -1,6 +0,0 @@ -accessWidener v1 named - -extendable method net/minecraft/client/gui/components/AbstractSelectionList children ()Ljava/util/List; -extendable method net/minecraft/client/gui/components/AbstractSelectionList getEntryAtPosition (DD)Lnet/minecraft/client/gui/components/AbstractSelectionList$Entry; -accessible class net/minecraft/client/gui/components/AbstractSelectionList$Entry -extendable method net/minecraft/client/gui/components/AbstractButton getTextureY ()I diff --git a/src/testmod/java/dev/isxander/yacl/test/ExampleMod.java b/src/testmod/java/dev/isxander/yacl/test/ExampleMod.java deleted file mode 100644 index 46c5225..0000000 --- a/src/testmod/java/dev/isxander/yacl/test/ExampleMod.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.isxander.yacl.test; - -import dev.isxander.yacl.test.config.ExampleConfig; -import net.fabricmc.api.ClientModInitializer; - -public class ExampleMod implements ClientModInitializer { - @Override - public void onInitializeClient() { - ExampleConfig.INSTANCE.load(); - } -} diff --git a/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java b/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java deleted file mode 100644 index 717132b..0000000 --- a/src/testmod/java/dev/isxander/yacl/test/config/ExampleConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.isxander.yacl.test.config; - -import dev.isxander.yacl.config.ConfigEntry; -import dev.isxander.yacl.config.ConfigInstance; -import dev.isxander.yacl.config.GsonConfigInstance; - -import java.awt.*; -import java.util.List; -import java.nio.file.Path; - -public class ExampleConfig { - public static final ConfigInstance INSTANCE = GsonConfigInstance.createBuilder(ExampleConfig.class) - .setPath(Path.of("./config/yacl-test.json")) - .build(); - - @ConfigEntry public boolean booleanToggle = false; - @ConfigEntry public boolean customBooleanToggle = false; - @ConfigEntry public boolean tickbox = false; - @ConfigEntry public int intSlider = 0; - @ConfigEntry public double doubleSlider = 0; - @ConfigEntry public float floatSlider = 0; - @ConfigEntry public long longSlider = 0; - @ConfigEntry public String textField = "Hello"; - @ConfigEntry public Color colorOption = Color.red; - @ConfigEntry public double doubleField = 0.5; - @ConfigEntry public float floatField = 0.5f; - @ConfigEntry public int intField = 5; - @ConfigEntry public long longField = 5; - @ConfigEntry public Alphabet enumOption = Alphabet.A; - - @ConfigEntry public List stringList = List.of("This is quite cool.", "You can add multiple items!", "And it is integrated so well into Option groups!"); - @ConfigEntry public List intList = List.of(1, 2, 3); - - @ConfigEntry public boolean groupTestRoot = false; - @ConfigEntry public boolean groupTestFirstGroup = false; - @ConfigEntry public boolean groupTestFirstGroup2 = false; - @ConfigEntry public boolean groupTestSecondGroup = false; - - @ConfigEntry public int scrollingSlider = 0; - - public enum Alphabet { - A, B, C - } -} diff --git a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java b/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java deleted file mode 100644 index 18d2a3c..0000000 --- a/src/testmod/java/dev/isxander/yacl/test/config/GuiTest.java +++ /dev/null @@ -1,449 +0,0 @@ -package dev.isxander.yacl.test.config; - -import dev.isxander.yacl.api.*; -import dev.isxander.yacl.gui.RequireRestartScreen; -import dev.isxander.yacl.gui.controllers.*; -import dev.isxander.yacl.gui.controllers.cycling.EnumController; -import dev.isxander.yacl.gui.controllers.slider.DoubleSliderController; -import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; -import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; -import dev.isxander.yacl.gui.controllers.slider.LongSliderController; -import dev.isxander.yacl.gui.controllers.string.StringController; -import dev.isxander.yacl.gui.controllers.string.number.DoubleFieldController; -import dev.isxander.yacl.gui.controllers.string.number.FloatFieldController; -import dev.isxander.yacl.gui.controllers.string.number.IntegerFieldController; -import dev.isxander.yacl.gui.controllers.string.number.LongFieldController; -import net.minecraft.client.GraphicsStatus; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.components.toasts.SystemToast; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.network.chat.ClickEvent; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.HoverEvent; - -import java.awt.Color; -import java.util.List; - -public class GuiTest { - public static Screen getModConfigScreenFactory(Screen parent) { - return YetAnotherConfigLib.create(ExampleConfig.INSTANCE, (defaults, config, builder) -> builder - .title(Component.literal("Test Suites")) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Suites")) - .option(ButtonOption.createBuilder() - .name(Component.literal("Full Test Suite")) - .controller(ActionController::new) - .action((screen, opt) -> Minecraft.getInstance().setScreen(getFullTestSuite(screen))) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Wiki")) - .option(ButtonOption.createBuilder() - .name(Component.literal("Get Started")) - .controller(ActionController::new) - .action((screen, opt) -> Minecraft.getInstance().setScreen(getWikiGetStarted(screen))) - .build()) - .build()) - .build()) - ) - .generateScreen(parent); - } - - private static Screen getFullTestSuite(Screen parent) { - return YetAnotherConfigLib.create(ExampleConfig.INSTANCE, (defaults, config, builder) -> builder - .title(Component.literal("Test GUI")) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Control Examples")) - .tooltip(Component.literal("Example Category Description")) - .group(OptionGroup.createBuilder() - .name(Component.literal("Boolean Controllers")) - .tooltip(Component.literal("Test!")) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Boolean Toggle")) - .tooltip(value -> Component.literal("A simple toggle button that contains the value '" + value + "'")) - .binding( - defaults.booleanToggle, - () -> config.booleanToggle, - (value) -> config.booleanToggle = value - ) - .controller(BooleanController::new) - .flag(OptionFlag.GAME_RESTART) - .build()) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Custom Boolean Toggle")) - .tooltip(Component.literal("You can customize these controllers like this!")) - .tooltip(Component.empty()) - .tooltip(opt -> Component.empty()) - .binding( - defaults.customBooleanToggle, - () -> config.customBooleanToggle, - (value) -> config.customBooleanToggle = value - ) - .controller(opt -> new BooleanController(opt, state -> state ? Component.literal("Amazing") : Component.literal("Not Amazing"), true)) - .build()) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Tick Box")) - .tooltip(Component.literal("There are even alternate methods of displaying the same data type!")) - .binding( - defaults.tickbox, - () -> config.tickbox, - (value) -> config.tickbox = value - ) - .controller(TickBoxController::new) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Slider Controllers")) - .option(Option.createBuilder(int.class) - .name(Component.literal("Int Slider")) - .instant(true) - .binding( - defaults.intSlider, - () -> config.intSlider, - value -> config.intSlider = value - - ) - .controller(opt -> new IntegerSliderController(opt, 0, 3, 1)) - .build()) - .option(Option.createBuilder(double.class) - .name(Component.literal("Double Slider")) - .binding( - defaults.doubleSlider, - () -> config.doubleSlider, - (value) -> config.doubleSlider = value - ) - .controller(opt -> new DoubleSliderController(opt, 0, 3, 0.05)) - .build()) - .option(Option.createBuilder(float.class) - .name(Component.literal("Float Slider")) - .binding( - defaults.floatSlider, - () -> config.floatSlider, - (value) -> config.floatSlider = value - ) - .controller(opt -> new FloatSliderController(opt, 0, 3, 0.1f)) - .build()) - .option(Option.createBuilder(long.class) - .name(Component.literal("Long Slider")) - .binding( - defaults.longSlider, - () -> config.longSlider, - (value) -> config.longSlider = value - ) - .controller(opt -> new LongSliderController(opt, 0, 1_000_000, 100)) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Input Field Controllers")) - .option(Option.createBuilder(String.class) - .name(Component.literal("Component Option")) - .binding( - defaults.textField, - () -> config.textField, - value -> config.textField = value - ) - .controller(StringController::new) - .build()) - .option(Option.createBuilder(Color.class) - .name(Component.literal("Color Option")) - .binding( - defaults.colorOption, - () -> config.colorOption, - value -> config.colorOption = value - ) - .controller(ColorController::new) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Number Fields")) - .option(Option.createBuilder(double.class) - .name(Component.literal("Double Field")) - .binding( - defaults.doubleField, - () -> config.doubleField, - value -> config.doubleField = value - ) - .controller(DoubleFieldController::new) - .build()) - .option(Option.createBuilder(float.class) - .name(Component.literal("Float Field")) - .binding( - defaults.floatField, - () -> config.floatField, - value -> config.floatField = value - ) - .controller(FloatFieldController::new) - .build()) - .option(Option.createBuilder(int.class) - .name(Component.literal("Integer Field")) - .binding( - defaults.intField, - () -> config.intField, - value -> config.intField = value - ) - .controller(IntegerFieldController::new) - .build()) - .option(Option.createBuilder(long.class) - .name(Component.literal("Long Field")) - .binding( - defaults.longField, - () -> config.longField, - value -> config.longField = value - ) - .controller(LongFieldController::new) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Enum Controllers")) - .option(Option.createBuilder(ExampleConfig.Alphabet.class) - .name(Component.literal("Enum Cycler")) - .binding( - defaults.enumOption, - () -> config.enumOption, - (value) -> config.enumOption = value - ) - .controller(EnumController::new) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Options that aren't really options")) - .option(ButtonOption.createBuilder() - .name(Component.literal("Button \"Option\"")) - .action((screen, opt) -> SystemToast.add(Minecraft.getInstance().getToasts(), SystemToast.SystemToastIds.TUTORIAL_HINT, Component.literal("Button Pressed"), Component.literal("Button option was invoked!"))) - .controller(ActionController::new) - .build()) - .option(LabelOption.create( - Component.empty() - .append(Component.literal("a").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("a"))))) - .append(Component.literal("b").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("b"))))) - .append(Component.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("c"))))) - .append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e"))))) - .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev")))) - ) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("Minecraft Bindings")) - .tooltip(Component.literal("YACL can also bind Minecraft options!")) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Minecraft AutoJump")) - .tooltip(Component.literal("You can even bind minecraft options!")) - .binding(Binding.minecraft(Minecraft.getInstance().options.autoJump())) - .controller(TickBoxController::new) - .build()) - .option(Option.createBuilder(GraphicsStatus.class) - .name(Component.literal("Minecraft Graphics Mode")) - .binding(Binding.minecraft(Minecraft.getInstance().options.graphicsMode())) - .controller(EnumController::new) - .build()) - .build()) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("List Test")) - .group(ListOption.createBuilder(String.class) - .name(Component.literal("String List")) - .binding( - defaults.stringList, - () -> config.stringList, - val -> config.stringList = val - ) - .controller(StringController::new) - .initial("") - .build()) - .group(ListOption.createBuilder(Integer.class) - .name(Component.literal("Slider List")) - .binding( - defaults.intList, - () -> config.intList, - val -> config.intList = val - ) - .controller(opt -> new IntegerSliderController(opt, 0, 10, 1)) - .initial(0) - .available(false) - .build()) - .group(ListOption.createBuilder(Component.class) - .name(Component.literal("Useless Label List")) - .binding(Binding.immutable(List.of(Component.literal("It's quite impressive that literally every single controller works, without problem.")))) - .controller(LabelController::new) - .initial(Component.literal("Initial label")) - .build()) - .build()) - .category(PlaceholderCategory.createBuilder() - .name(Component.literal("Placeholder Category")) - .screen((client, yaclScreen) -> new RequireRestartScreen(yaclScreen)) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Group Test")) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Root Test")) - .binding( - defaults.groupTestRoot, - () -> config.groupTestRoot, - value -> config.groupTestRoot = value - ) - .controller(TickBoxController::new) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.literal("First Group")) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("First Group Test 1")) - .binding( - defaults.groupTestFirstGroup, - () -> config.groupTestFirstGroup, - value -> config.groupTestFirstGroup = value - ) - .controller(TickBoxController::new) - .build()) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("First Group Test 2")) - .binding( - defaults.groupTestFirstGroup2, - () -> config.groupTestFirstGroup2, - value -> config.groupTestFirstGroup2 = value - ) - .controller(TickBoxController::new) - .build()) - .build()) - .group(OptionGroup.createBuilder() - .name(Component.empty()) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Second Group Test")) - .binding( - defaults.groupTestSecondGroup, - () -> config.groupTestSecondGroup, - value -> config.groupTestSecondGroup = value - ) - .controller(TickBoxController::new) - .build()) - .build()) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Scroll Test")) - .option(Option.createBuilder(int.class) - .name(Component.literal("Int Slider that is cut off because the slider")) - .binding( - defaults.scrollingSlider, - () -> config.scrollingSlider, - (value) -> config.scrollingSlider = value - ) - .controller(opt -> new IntegerSliderController(opt, 0, 10, 1)) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .option(ButtonOption.createBuilder() - .name(Component.literal("Option")) - .action((screen, opt) -> {}) - .controller(ActionController::new) - .build()) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Category Test")) - .build()) - .save(() -> { - Minecraft.getInstance().options.save(); - ExampleConfig.INSTANCE.save(); - }) - ) - .generateScreen(parent); - } - - private static boolean myBooleanOption = true; - - private static Screen getWikiGetStarted(Screen parent) { - return YetAnotherConfigLib.createBuilder() - .title(Component.literal("Used for narration. Could be used to render a title in the future.")) - .category(ConfigCategory.createBuilder() - .name(Component.literal("Name of the category")) - .tooltip(Component.literal("This Component will appear as a tooltip when you hover or focus the button with Tab. There is no need to add \n to wrap as YACL will do it for you.")) - .group(OptionGroup.createBuilder() - .name(Component.literal("Name of the group")) - .tooltip(Component.literal("This Component will appear when you hover over the name or focus on the collapse button with Tab.")) - .option(Option.createBuilder(boolean.class) - .name(Component.literal("Boolean Option")) - .tooltip(Component.literal("This Component will appear as a tooltip when you hover over the option.")) - .binding(true, () -> myBooleanOption, newVal -> myBooleanOption = newVal) - .controller(TickBoxController::new) - .build()) - .build()) - .build()) - .build() - .generateScreen(parent); - } -} diff --git a/src/testmod/java/dev/isxander/yacl/test/mixins/TitleScreenMixin.java b/src/testmod/java/dev/isxander/yacl/test/mixins/TitleScreenMixin.java deleted file mode 100644 index eac9ff4..0000000 --- a/src/testmod/java/dev/isxander/yacl/test/mixins/TitleScreenMixin.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.isxander.yacl.test.mixins; - -import dev.isxander.yacl.test.config.GuiTest; -import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.TitleScreen; -import net.minecraft.network.chat.Component; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -/** - * For testing purposes! If you are using this as - * an example, ignore this class! - */ -@Mixin(TitleScreen.class) -public abstract class TitleScreenMixin extends Screen { - protected TitleScreenMixin(Component title) { - super(title); - } - - @Inject(method = "init", at = @At("RETURN")) - private void injectTestButton(CallbackInfo ci) { - addRenderableWidget(Button.builder(Component.literal("YACL"), button -> minecraft.setScreen(GuiTest.getModConfigScreenFactory(minecraft.screen))) - .pos(0, 0) - .width(50) - .build()); - } -} diff --git a/src/testmod/resources/fabric.mod.json b/src/testmod/resources/fabric.mod.json deleted file mode 100644 index d682084..0000000 --- a/src/testmod/resources/fabric.mod.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "schemaVersion": 1, - "id": "test-mod", - "version": "1", - "name": "YACL Test", - "authors": [ - "isXander" - ], - "license": "LGPL-3.0-or-later", - "environment": "client", - "entrypoints": { - "client": [ - "dev.isxander.yacl.test.ExampleMod" - ] - }, - "depends": { - "yet-another-config-lib": "*" - }, - "mixins": [ - "yet-another-config-lib.test.mixins.json" - ] -} diff --git a/src/testmod/resources/yet-another-config-lib.test.mixins.json b/src/testmod/resources/yet-another-config-lib.test.mixins.json deleted file mode 100644 index 9287cdd..0000000 --- a/src/testmod/resources/yet-another-config-lib.test.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "dev.isxander.yacl.test.mixins", - "compatibilityLevel": "JAVA_17", - "injectors": { - "defaultRequire": 1 - }, - "client": [ - "TitleScreenMixin" - ] -} diff --git a/test-common/.gitignore b/test-common/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/test-common/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/test-common/build.gradle.kts b/test-common/build.gradle.kts new file mode 100644 index 0000000..7e14666 --- /dev/null +++ b/test-common/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.architectury.loom) +} + +architectury { + common("fabric", "forge") +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(project(":common").loom.accessWidenerPath) +} + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:${libs.versions.minecraft.get()}+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + modImplementation(libs.fabric.loader) + + implementation(project(path = ":common", configuration = "namedElements")) +} diff --git a/test-common/src/main/java/dev/isxander/yacl/test/ConfigTest.java b/test-common/src/main/java/dev/isxander/yacl/test/ConfigTest.java new file mode 100644 index 0000000..71cb285 --- /dev/null +++ b/test-common/src/main/java/dev/isxander/yacl/test/ConfigTest.java @@ -0,0 +1,51 @@ +package dev.isxander.yacl.test; + +import dev.isxander.yacl.config.ConfigEntry; +import dev.isxander.yacl.config.ConfigInstance; +import dev.isxander.yacl.config.GsonConfigInstance; + +import java.awt.*; +import java.util.List; +import java.nio.file.Path; + +public class ConfigTest { + public static final ConfigInstance GSON = GsonConfigInstance.createBuilder(ConfigTest.class) + .setPath(Path.of("./config/yacl-test.json")) + .build(); + + @ConfigEntry public boolean booleanToggle = false; + @ConfigEntry public boolean customBooleanToggle = false; + @ConfigEntry public boolean tickbox = false; + @ConfigEntry public int intSlider = 0; + @ConfigEntry public double doubleSlider = 0; + @ConfigEntry public float floatSlider = 0; + @ConfigEntry public long longSlider = 0; + @ConfigEntry public String textField = "Hello"; + @ConfigEntry public Color colorOption = Color.red; + @ConfigEntry public double doubleField = 0.5; + @ConfigEntry public float floatField = 0.5f; + @ConfigEntry public int intField = 5; + @ConfigEntry public long longField = 5; + @ConfigEntry public Alphabet enumOption = Alphabet.A; + + @ConfigEntry + public List stringList = List.of("This is quite cool.", "You can add multiple items!", "And it is integrated so well into Option groups!"); + @ConfigEntry + public List intList = List.of(1, 2, 3); + + @ConfigEntry + public boolean groupTestRoot = false; + @ConfigEntry + public boolean groupTestFirstGroup = false; + @ConfigEntry + public boolean groupTestFirstGroup2 = false; + @ConfigEntry + public boolean groupTestSecondGroup = false; + + @ConfigEntry + public int scrollingSlider = 0; + + public enum Alphabet { + A, B, C + } +} diff --git a/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java new file mode 100644 index 0000000..9065e76 --- /dev/null +++ b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java @@ -0,0 +1,462 @@ +package dev.isxander.yacl.test; + +import dev.isxander.yacl.api.*; +import dev.isxander.yacl.gui.RequireRestartScreen; +import dev.isxander.yacl.gui.controllers.*; +import dev.isxander.yacl.gui.controllers.cycling.EnumController; +import dev.isxander.yacl.gui.controllers.slider.DoubleSliderController; +import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; +import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; +import dev.isxander.yacl.gui.controllers.slider.LongSliderController; +import dev.isxander.yacl.gui.controllers.string.StringController; +import dev.isxander.yacl.gui.controllers.string.number.DoubleFieldController; +import dev.isxander.yacl.gui.controllers.string.number.FloatFieldController; +import dev.isxander.yacl.gui.controllers.string.number.IntegerFieldController; +import dev.isxander.yacl.gui.controllers.string.number.LongFieldController; +import net.minecraft.client.GraphicsStatus; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; + +import java.awt.Color; +import java.util.List; + +public class GuiTest { + public static Screen getModConfigScreenFactory(Screen parent) { + return YetAnotherConfigLib.create(ConfigTest.GSON, (defaults, config, builder) -> builder + .title(Component.literal("Test Suites")) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Suites")) + .option(ButtonOption.createBuilder() + .name(Component.literal("Full Test Suite")) + .controller(ActionController::new) + .action((screen, opt) -> Minecraft.getInstance().setScreen(getFullTestSuite(screen))) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Wiki")) + .option(ButtonOption.createBuilder() + .name(Component.literal("Get Started")) + .controller(ActionController::new) + .action((screen, opt) -> Minecraft.getInstance().setScreen(getWikiGetStarted(screen))) + .build()) + .build()) + .build()) + ) + .generateScreen(parent); + } + + private static Screen getFullTestSuite(Screen parent) { + return YetAnotherConfigLib.create(ConfigTest.GSON, (defaults, config, builder) -> builder + .title(Component.literal("Test GUI")) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Control Examples")) + .tooltip(Component.literal("Example Category Description")) + .group(OptionGroup.createBuilder() + .name(Component.literal("Boolean Controllers")) + .tooltip(Component.literal("Test!")) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Boolean Toggle")) + .tooltip(value -> Component.literal("A simple toggle button that contains the value '" + value + "'")) + .binding( + defaults.booleanToggle, + () -> config.booleanToggle, + (value) -> config.booleanToggle = value + ) + .controller(BooleanController::new) + .flag(OptionFlag.GAME_RESTART) + .build()) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Custom Boolean Toggle")) + .tooltip(Component.literal("You can customize these controllers like this!")) + .tooltip(Component.empty()) + .tooltip(opt -> Component.empty()) + .binding( + defaults.customBooleanToggle, + () -> config.customBooleanToggle, + (value) -> config.customBooleanToggle = value + ) + .controller(opt -> new BooleanController(opt, state -> state ? Component.literal("Amazing") : Component.literal("Not Amazing"), true)) + .build()) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Tick Box")) + .tooltip(Component.literal("There are even alternate methods of displaying the same data type!")) + .binding( + defaults.tickbox, + () -> config.tickbox, + (value) -> config.tickbox = value + ) + .controller(TickBoxController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Slider Controllers")) + .option(Option.createBuilder(int.class) + .name(Component.literal("Int Slider")) + .instant(true) + .binding( + defaults.intSlider, + () -> config.intSlider, + value -> config.intSlider = value + + ) + .controller(opt -> new IntegerSliderController(opt, 0, 3, 1)) + .build()) + .option(Option.createBuilder(double.class) + .name(Component.literal("Double Slider")) + .binding( + defaults.doubleSlider, + () -> config.doubleSlider, + (value) -> config.doubleSlider = value + ) + .controller(opt -> new DoubleSliderController(opt, 0, 3, 0.05)) + .build()) + .option(Option.createBuilder(float.class) + .name(Component.literal("Float Slider")) + .binding( + defaults.floatSlider, + () -> config.floatSlider, + (value) -> config.floatSlider = value + ) + .controller(opt -> new FloatSliderController(opt, 0, 3, 0.1f)) + .build()) + .option(Option.createBuilder(long.class) + .name(Component.literal("Long Slider")) + .binding( + defaults.longSlider, + () -> config.longSlider, + (value) -> config.longSlider = value + ) + .controller(opt -> new LongSliderController(opt, 0, 1_000_000, 100)) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Input Field Controllers")) + .option(Option.createBuilder(String.class) + .name(Component.literal("Component Option")) + .binding( + defaults.textField, + () -> config.textField, + value -> config.textField = value + ) + .controller(StringController::new) + .build()) + .option(Option.createBuilder(Color.class) + .name(Component.literal("Color Option")) + .binding( + defaults.colorOption, + () -> config.colorOption, + value -> config.colorOption = value + ) + .controller(ColorController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Number Fields")) + .option(Option.createBuilder(double.class) + .name(Component.literal("Double Field")) + .binding( + defaults.doubleField, + () -> config.doubleField, + value -> config.doubleField = value + ) + .controller(DoubleFieldController::new) + .build()) + .option(Option.createBuilder(float.class) + .name(Component.literal("Float Field")) + .binding( + defaults.floatField, + () -> config.floatField, + value -> config.floatField = value + ) + .controller(FloatFieldController::new) + .build()) + .option(Option.createBuilder(int.class) + .name(Component.literal("Integer Field")) + .binding( + defaults.intField, + () -> config.intField, + value -> config.intField = value + ) + .controller(IntegerFieldController::new) + .build()) + .option(Option.createBuilder(long.class) + .name(Component.literal("Long Field")) + .binding( + defaults.longField, + () -> config.longField, + value -> config.longField = value + ) + .controller(LongFieldController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Enum Controllers")) + .option(Option.createBuilder(ConfigTest.Alphabet.class) + .name(Component.literal("Enum Cycler")) + .binding( + defaults.enumOption, + () -> config.enumOption, + (value) -> config.enumOption = value + ) + .controller(EnumController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Options that aren't really options")) + .option(ButtonOption.createBuilder() + .name(Component.literal("Button \"Option\"")) + .action((screen, opt) -> SystemToast.add(Minecraft.getInstance().getToasts(), SystemToast.SystemToastIds.TUTORIAL_HINT, Component.literal("Button Pressed"), Component.literal("Button option was invoked!"))) + .controller(ActionController::new) + .build()) + .option(LabelOption.create( + Component.empty() + .append(Component.literal("a").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("a"))))) + .append(Component.literal("b").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("b"))))) + .append(Component.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("c"))))) + .append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e"))))) + .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev")))) + ) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("Minecraft Bindings")) + .tooltip(Component.literal("YACL can also bind Minecraft options!")) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Minecraft AutoJump")) + .tooltip(Component.literal("You can even bind minecraft options!")) + .binding(Binding.minecraft(Minecraft.getInstance().options.autoJump())) + .controller(TickBoxController::new) + .build()) + .option(Option.createBuilder(GraphicsStatus.class) + .name(Component.literal("Minecraft Graphics Mode")) + .binding(Binding.minecraft(Minecraft.getInstance().options.graphicsMode())) + .controller(EnumController::new) + .build()) + .build()) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("List Test")) + .group(ListOption.createBuilder(String.class) + .name(Component.literal("String List")) + .binding( + defaults.stringList, + () -> config.stringList, + val -> config.stringList = val + ) + .controller(StringController::new) + .initial("") + .build()) + .group(ListOption.createBuilder(Integer.class) + .name(Component.literal("Slider List")) + .binding( + defaults.intList, + () -> config.intList, + val -> config.intList = val + ) + .controller(opt -> new IntegerSliderController(opt, 0, 10, 1)) + .initial(0) + .available(false) + .build()) + .group(ListOption.createBuilder(Component.class) + .name(Component.literal("Useless Label List")) + .binding(Binding.immutable(List.of(Component.literal("It's quite impressive that literally every single controller works, without problem.")))) + .controller(LabelController::new) + .initial(Component.literal("Initial label")) + .build()) + .build()) + .category(PlaceholderCategory.createBuilder() + .name(Component.literal("Placeholder Category")) + .screen((client, yaclScreen) -> new RequireRestartScreen(yaclScreen)) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Group Test")) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Root Test")) + .binding( + defaults.groupTestRoot, + () -> config.groupTestRoot, + value -> config.groupTestRoot = value + ) + .controller(TickBoxController::new) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.literal("First Group")) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("First Group Test 1")) + .binding( + defaults.groupTestFirstGroup, + () -> config.groupTestFirstGroup, + value -> config.groupTestFirstGroup = value + ) + .controller(TickBoxController::new) + .build()) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("First Group Test 2")) + .binding( + defaults.groupTestFirstGroup2, + () -> config.groupTestFirstGroup2, + value -> config.groupTestFirstGroup2 = value + ) + .controller(TickBoxController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() + .name(Component.empty()) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Second Group Test")) + .binding( + defaults.groupTestSecondGroup, + () -> config.groupTestSecondGroup, + value -> config.groupTestSecondGroup = value + ) + .controller(TickBoxController::new) + .build()) + .build()) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Scroll Test")) + .option(Option.createBuilder(int.class) + .name(Component.literal("Int Slider that is cut off because the slider")) + .binding( + defaults.scrollingSlider, + () -> config.scrollingSlider, + (value) -> config.scrollingSlider = value + ) + .controller(opt -> new IntegerSliderController(opt, 0, 10, 1)) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .option(ButtonOption.createBuilder() + .name(Component.literal("Option")) + .action((screen, opt) -> { + }) + .controller(ActionController::new) + .build()) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Category Test")) + .build()) + .save(() -> { + Minecraft.getInstance().options.save(); + ConfigTest.GSON.save(); + }) + ) + .generateScreen(parent); + } + + private static boolean myBooleanOption = true; + + private static Screen getWikiGetStarted(Screen parent) { + return YetAnotherConfigLib.createBuilder() + .title(Component.literal("Used for narration. Could be used to render a title in the future.")) + .category(ConfigCategory.createBuilder() + .name(Component.literal("Name of the category")) + .tooltip(Component.literal("This Component will appear as a tooltip when you hover or focus the button with Tab. There is no need to add \n to wrap as YACL will do it for you.")) + .group(OptionGroup.createBuilder() + .name(Component.literal("Name of the group")) + .tooltip(Component.literal("This Component will appear when you hover over the name or focus on the collapse button with Tab.")) + .option(Option.createBuilder(boolean.class) + .name(Component.literal("Boolean Option")) + .tooltip(Component.literal("This Component will appear as a tooltip when you hover over the option.")) + .binding(true, () -> myBooleanOption, newVal -> myBooleanOption = newVal) + .controller(TickBoxController::new) + .build()) + .build()) + .build()) + .build() + .generateScreen(parent); + } +} diff --git a/test-fabric/.gitignore b/test-fabric/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/test-fabric/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/test-fabric/build.gradle.kts b/test-fabric/build.gradle.kts new file mode 100644 index 0000000..1d2d5fd --- /dev/null +++ b/test-fabric/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(libs.plugins.architectury.loom) + alias(libs.plugins.shadow) +} + +architectury { + platformSetupLoomIde() + fabric() +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(project(":common").loom.accessWidenerPath) +} + +val common by configurations.registering +val shadowCommon by configurations.registering +configurations.compileClasspath.get().extendsFrom(common.get()) +configurations["developmentFabric"].extendsFrom(common.get()) + +val minecraftVersion = libs.versions.minecraft.get() + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + modImplementation(libs.fabric.loader) + + "common"(project(path = ":test-common", configuration = "namedElements")) { isTransitive = false } + implementation(project(path = ":fabric", configuration = "namedElements")) + + "common"(project(path = ":common", configuration = "namedElements")) { isTransitive = false } +} + +tasks { + shadowJar { + exclude("architectury.common.json") + + configurations = listOf(shadowCommon.get()) + archiveClassifier.set("dev-shadow") + } + + remapJar { + injectAccessWidener.set(true) + inputFile.set(shadowJar.get().archiveFile) + dependsOn(shadowJar) + + archiveClassifier.set("fabric-$minecraftVersion") + } + + jar { + archiveClassifier.set("dev") + } +} diff --git a/test-fabric/src/main/java/dev/isxander/yacl/test/fabric/ModMenuEntrypoint.java b/test-fabric/src/main/java/dev/isxander/yacl/test/fabric/ModMenuEntrypoint.java new file mode 100644 index 0000000..b2dc876 --- /dev/null +++ b/test-fabric/src/main/java/dev/isxander/yacl/test/fabric/ModMenuEntrypoint.java @@ -0,0 +1,12 @@ +package dev.isxander.yacl.test.fabric; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import dev.isxander.yacl.test.GuiTest; + +public class ModMenuEntrypoint implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return GuiTest::getModConfigScreenFactory; + } +} diff --git a/test-fabric/src/main/resources/fabric.mod.json b/test-fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..15e28a4 --- /dev/null +++ b/test-fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "id": "yacl_test", + "version": "1", + "name": "YACL Test", + "license": "LGPL-3.0-or-later", + "environment": "*", + "entrypoints": { + "modmenu": [ + "dev.isxander.yacl.test.fabric.ModMenuEntrypoint" + ] + } +} diff --git a/test-forge/.gitignore b/test-forge/.gitignore new file mode 100644 index 0000000..b63da45 --- /dev/null +++ b/test-forge/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/test-forge/build.gradle.kts b/test-forge/build.gradle.kts new file mode 100644 index 0000000..5f98b90 --- /dev/null +++ b/test-forge/build.gradle.kts @@ -0,0 +1,63 @@ +plugins { + alias(libs.plugins.architectury.loom) + alias(libs.plugins.shadow) +} + +architectury { + platformSetupLoomIde() + forge() +} + +loom { + silentMojangMappingsLicense() + + accessWidenerPath.set(project(":common").loom.accessWidenerPath) + + forge { + convertAccessWideners.set(true) + extraAccessWideners.add(loom.accessWidenerPath.get().asFile.name) + } +} + +val common by configurations.registering +val shadowCommon by configurations.registering +configurations.compileClasspath.get().extendsFrom(common.get()) +configurations["developmentForge"].extendsFrom(common.get()) + +val minecraftVersion: String = libs.versions.minecraft.get() + +dependencies { + minecraft(libs.minecraft) + mappings(loom.layered { + mappings("org.quiltmc:quilt-mappings:$minecraftVersion+build.${libs.versions.quilt.mappings.get()}:intermediary-v2") + officialMojangMappings() + }) + forge(libs.forge) + + "common"(project(path = ":test-common", configuration = "namedElements")) { isTransitive = false } + implementation(project(path = ":forge", configuration = "namedElements")) + + "common"(project(path = ":common", configuration = "namedElements")) { isTransitive = false } +} + +tasks { + shadowJar { + exclude("fabric.mod.json") + exclude("architectury.common.json") + + configurations = listOf(shadowCommon.get()) + archiveClassifier.set("dev-shadow") + } + + remapJar { + injectAccessWidener.set(true) + inputFile.set(shadowJar.get().archiveFile) + dependsOn(shadowJar) + + archiveClassifier.set("forge-$minecraftVersion") + } + + jar { + archiveClassifier.set("dev") + } +} diff --git a/test-forge/gradle.properties b/test-forge/gradle.properties new file mode 100644 index 0000000..32f842a --- /dev/null +++ b/test-forge/gradle.properties @@ -0,0 +1 @@ +loom.platform=forge \ No newline at end of file diff --git a/test-forge/src/main/java/dev/isxander/yacl/test/forge/ForgeTest.java b/test-forge/src/main/java/dev/isxander/yacl/test/forge/ForgeTest.java new file mode 100644 index 0000000..182c4a9 --- /dev/null +++ b/test-forge/src/main/java/dev/isxander/yacl/test/forge/ForgeTest.java @@ -0,0 +1,18 @@ +package dev.isxander.yacl.test.forge; + +import dev.isxander.yacl.test.GuiTest; +import net.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; + +@Mod("yacl_test") +public class ForgeTest { + public ForgeTest() { + ModLoadingContext.get().registerExtensionPoint( + ConfigScreenHandler.ConfigScreenFactory.class, + () -> new ConfigScreenHandler.ConfigScreenFactory( + (minecraft, parent) -> GuiTest.getModConfigScreenFactory(parent) + ) + ); + } +} diff --git a/test-forge/src/main/resources/META-INF/mods.toml b/test-forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..f4f57d0 --- /dev/null +++ b/test-forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,28 @@ +modLoader = "javafml" +loaderVersion = "[45,)" +#issueTrackerURL = "" +license = "LGPL-3.0-or-later" + +[[mods]] +modId = "yacl_test" +version = "1" +displayName = "YACL Test" +authors = "isXander" +description = ''' +Test mod for YACL +''' +#logoFile = "" + +[[dependencies.yacl_test]] +modId = "forge" +mandatory = true +versionRange = "[45,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.yacl_test]] +modId = "minecraft" +mandatory = true +versionRange = "[1.19.4,)" +ordering = "NONE" +side = "BOTH" diff --git a/test-forge/src/main/resources/pack.mcmeta b/test-forge/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..8fbe236 --- /dev/null +++ b/test-forge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "Test YACL", + "pack_format": 14 + } +} -- cgit