From 6ef0f3758d17b5fd7e2ddef62c220ec10e7269c6 Mon Sep 17 00:00:00 2001 From: Shiewk Date: Sat, 7 Sep 2024 18:54:20 +0200 Subject: [PATCH] Initial Commit (1.0) --- .gitignore | 63 ++++++ LICENSE | 165 +++++++++++++++ README.md | 42 ++++ build.gradle | 106 ++++++++++ gradle.properties | 14 ++ settings.gradle | 9 + .../java/de/shiewk/widgets/Dimensionable.java | 8 + .../java/de/shiewk/widgets/ModMenuConfig.java | 13 ++ .../java/de/shiewk/widgets/ModWidget.java | 45 ++++ .../shiewk/widgets/WidgetSettingOption.java | 86 ++++++++ .../de/shiewk/widgets/WidgetSettings.java | 104 +++++++++ .../java/de/shiewk/widgets/WidgetUtils.java | 18 ++ .../java/de/shiewk/widgets/WidgetsMod.java | 14 ++ .../shiewk/widgets/client/WidgetManager.java | 98 +++++++++ .../shiewk/widgets/client/WidgetRenderer.java | 78 +++++++ .../widgets/client/WidgetsModClient.java | 62 ++++++ .../widgets/client/screen/AnimatedScreen.java | 39 ++++ .../screen/EditWidgetPositionsScreen.java | 198 ++++++++++++++++++ .../client/screen/WidgetConfigScreen.java | 129 ++++++++++++ .../client/screen/WidgetSettingsScreen.java | 50 +++++ .../screen/components/WidgetListWidget.java | 92 ++++++++ .../components/WidgetSettingsEditWidget.java | 135 ++++++++++++ .../screen/components/WidgetWidget.java | 130 ++++++++++++ .../de/shiewk/widgets/mixin/MixinMouse.java | 27 +++ .../widgets/widgets/BandwidthWidget.java | 73 +++++++ .../widgets/widgets/BasicTextWidget.java | 58 +++++ .../de/shiewk/widgets/widgets/CPSWidget.java | 136 ++++++++++++ .../shiewk/widgets/widgets/ClockWidget.java | 149 +++++++++++++ .../widgets/widgets/CoordinatesWidget.java | 115 ++++++++++ .../de/shiewk/widgets/widgets/FPSWidget.java | 28 +++ .../de/shiewk/widgets/widgets/PingWidget.java | 69 ++++++ .../widgets/widgets/PlayerCountWidget.java | 31 +++ .../widgets/widgets/ServerIPWidget.java | 47 +++++ .../widgets/settings/EnumWidgetSetting.java | 106 ++++++++++ .../settings/RGBAColorWidgetSetting.java | 123 +++++++++++ .../widgets/settings/ToggleWidgetSetting.java | 78 +++++++ src/main/resources/assets/widgets/icon.png | Bin 0 -> 46144 bytes .../resources/assets/widgets/lang/de_de.json | 51 +++++ .../resources/assets/widgets/lang/en_us.json | 51 +++++ src/main/resources/fabric.mod.json | 35 ++++ src/main/resources/widgets.accesswidener | 4 + src/main/resources/widgets.mixins.json | 14 ++ 42 files changed, 2893 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 settings.gradle create mode 100644 src/main/java/de/shiewk/widgets/Dimensionable.java create mode 100644 src/main/java/de/shiewk/widgets/ModMenuConfig.java create mode 100644 src/main/java/de/shiewk/widgets/ModWidget.java create mode 100644 src/main/java/de/shiewk/widgets/WidgetSettingOption.java create mode 100644 src/main/java/de/shiewk/widgets/WidgetSettings.java create mode 100644 src/main/java/de/shiewk/widgets/WidgetUtils.java create mode 100644 src/main/java/de/shiewk/widgets/WidgetsMod.java create mode 100644 src/main/java/de/shiewk/widgets/client/WidgetManager.java create mode 100644 src/main/java/de/shiewk/widgets/client/WidgetRenderer.java create mode 100644 src/main/java/de/shiewk/widgets/client/WidgetsModClient.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java create mode 100644 src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java create mode 100644 src/main/java/de/shiewk/widgets/mixin/MixinMouse.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/CPSWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/ClockWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/FPSWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/PingWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java create mode 100644 src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java create mode 100644 src/main/resources/assets/widgets/icon.png create mode 100644 src/main/resources/assets/widgets/lang/de_de.json create mode 100644 src/main/resources/assets/widgets/lang/en_us.json create mode 100644 src/main/resources/fabric.mod.json create mode 100644 src/main/resources/widgets.accesswidener create mode 100644 src/main/resources/widgets.mixins.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6dcdf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +.idea/ +*.iml +*.ipr +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +*.class +*.log +*.ctxt +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk +.gradle +build/ +gradle-app.setting +.gradletasknamecache +**/build/ +run/ +!gradle-wrapper.jar +gradle/ +gradlew +gradlew.bat \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..153d416 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3dfd9d5 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Widgets +**Adds customizable in-game widgets to your game.** + +![Screenshot of in-game hud](https://github.com/user-attachments/assets/362ffd66-75c2-487c-9d2a-21096ff2d042) + +This Minecraft mod adds customizable widgets that you may know from other Minecraft clients. + +This mod was specifically made for people who just use Fabric or Quilt (and no specific client). +It also has some benefits over using clients: +1. Performance: + - We've tried to optimize the mod as much as possible to make sure the game runs as smoothly as possible. + - Disabled widgets don't use any performance +2. No additional features + - The mod focuses on **widgets only**. It does not contain any other features such as performance improvements or mods. +3. Easy to use + - We've tried to make the mod as easy to use as possible, for example, there are three ways to open the config menu instead of only a keybind. + +## Widget settings +There are three ways to open the widget settings: +1. Press the key bind (bound to right shift by default) +2. Enter the command "/widgetsmod" +3. Open the config menu through [Mod Menu](https://modrinth.com/mod/modmenu) + +Once you've opened the settings, you will see something like this: + +![Widget Settings menu](https://github.com/user-attachments/assets/a8f8ac2d-6077-4de9-bf77-2968f8b7dbcf) + +You can now enable the widgets you want to enable, or click on them to view their additional settings, as you can see here: + +![CPS Widget Settings](https://github.com/user-attachments/assets/3703abb2-5f51-42dc-90e4-5847a9e0cc73) + +When you're done with configuring which widgets you want to enable and their settings, click the "Edit Layout" button in the bottom right corner. + +![Edit Layout button](https://github.com/user-attachments/assets/a788b082-ecb6-4a54-ba9c-cf513f128f2e) + +This will take you to a screen where you can freely move around your enabled widgets. + +![Edit Layout screen](https://github.com/user-attachments/assets/02f20215-8be1-4997-9a90-8ef7eb3662d9) + +When you're done, just press Escape until you return to the game. + +Now, everything is done. Your widgets automatically save for the next time you play. \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..51dff3c --- /dev/null +++ b/build.gradle @@ -0,0 +1,106 @@ +plugins { + id 'fabric-loom' version '1.6-SNAPSHOT' + id 'maven-publish' +} + +version = project.mod_version +group = project.maven_group + +base { + archivesName = project.archives_base_name +} + +loom { + accessWidenerPath = file("src/main/resources/widgets.accesswidener") +} + + +repositories { + // Add repositories to retrieve artifacts from in here. + // You should only use this when depending on other mods because + // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. + // See https://docs.gradle.org/current/userguide/declaring_repositories.html + // for more information about repositories. + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } +} + +dependencies { + // To change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + modImplementation "maven.modrinth:modmenu:11.0.2" +} + +processResources { + inputs.property "version", project.version + inputs.property "minecraft_version", project.minecraft_version + inputs.property "loader_version", project.loader_version + filteringCharset "UTF-8" + + filesMatching("fabric.mod.json") { + expand "version": project.version, + "minecraft_version": project.minecraft_version, + "loader_version": project.loader_version + } +} + +def targetJavaVersion = 21 +tasks.withType(JavaCompile).configureEach { + // ensure that the encoding is set to UTF-8, no matter what the system default is + // this fixes some edge cases with special characters not displaying correctly + // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html + // If Javadoc is generated, this must be specified in that task too. + it.options.encoding = "UTF-8" + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + it.options.release.set(targetJavaVersion) + } +} + +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } + // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task + // if it is present. + // If you remove this line, sources will not be generated. + withSourcesJar() +} + +jar { + from("LICENSE") { + rename { "${it}_${project.archivesBaseName}" } + } +} + +// configure the maven publication +publishing { + publications { + create("mavenJava", MavenPublication) { + artifactId = project.archives_base_name + from components.java + } + } + + // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. + repositories { + // Add repositories to publish to here. + // Notice: This block does NOT have the same function as the block in the top level. + // The repositories here will be used for publishing your artifact, not for + // retrieving dependencies. + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..3417289 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +# Fabric Properties +# check these on https://modmuss50.me/fabric.html +minecraft_version=1.21 +yarn_mappings=1.21+build.9 +loader_version=0.16.2 +# Mod Properties +mod_version=1.0 +maven_group=de.shiewk +archives_base_name=Widgets +# Dependencies +# check this on https://modmuss50.me/fabric.html +fabric_version=0.101.2+1.21 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f91a4fe --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/de/shiewk/widgets/Dimensionable.java b/src/main/java/de/shiewk/widgets/Dimensionable.java new file mode 100644 index 0000000..12dc49f --- /dev/null +++ b/src/main/java/de/shiewk/widgets/Dimensionable.java @@ -0,0 +1,8 @@ +package de.shiewk.widgets; + +public interface Dimensionable { + int width(); + int height(); + double getX(int mx); + double getY(int my); +} diff --git a/src/main/java/de/shiewk/widgets/ModMenuConfig.java b/src/main/java/de/shiewk/widgets/ModMenuConfig.java new file mode 100644 index 0000000..f2d26df --- /dev/null +++ b/src/main/java/de/shiewk/widgets/ModMenuConfig.java @@ -0,0 +1,13 @@ +package de.shiewk.widgets; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; + +public class ModMenuConfig implements ModMenuApi { + + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return WidgetConfigScreen::new; + } +} diff --git a/src/main/java/de/shiewk/widgets/ModWidget.java b/src/main/java/de/shiewk/widgets/ModWidget.java new file mode 100644 index 0000000..be7fa5c --- /dev/null +++ b/src/main/java/de/shiewk/widgets/ModWidget.java @@ -0,0 +1,45 @@ +package de.shiewk.widgets; + +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; +import java.util.Objects; + +public abstract class ModWidget implements Dimensionable { + + private final Identifier id; + private final WidgetSettings settings; + + protected ModWidget(Identifier id, List customSettings) { + Objects.requireNonNull(id, "id"); + this.id = id; + this.settings = WidgetSettings.ofId(id, customSettings); + } + + public final Identifier getId() { + return id; + } + + public final WidgetSettings getSettings() { + return settings; + } + public abstract void render(DrawContext context, long measuringTimeNano, TextRenderer textRenderer, int posX, int posY); + public abstract void tick(); + public abstract Text getName(); + public abstract Text getDescription(); + public abstract void onSettingsChanged(WidgetSettings settings); + + @Override + public double getX(int mx) { + return (int) WidgetUtils.translateToScreen(settings.posX, mx); + } + + @Override + public double getY(int my) { + return (int) WidgetUtils.translateToScreen(settings.posY, my); + } + +} diff --git a/src/main/java/de/shiewk/widgets/WidgetSettingOption.java b/src/main/java/de/shiewk/widgets/WidgetSettingOption.java new file mode 100644 index 0000000..d9067b3 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetSettingOption.java @@ -0,0 +1,86 @@ +package de.shiewk.widgets; + +import com.google.gson.JsonElement; +import net.minecraft.client.gui.Drawable; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.text.Text; + +import java.util.function.Consumer; + +public abstract class WidgetSettingOption implements Drawable, Widget { + private final String id; + private final Text name; + private int x = 0; + private int y = 0; + private boolean focused = false; + + protected WidgetSettingOption(String id, Text name) { + this.id = id; + this.name = name; + } + + public final String getId() { + return id; + } + + public final Text getName() { + return name; + } + + public abstract JsonElement saveState(); + public abstract void loadState(JsonElement state); + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return false; + } + + public boolean mouseReleased(double mouseX, double mouseY, int button){ + return false; + } + + public boolean charTyped(char chr, int modifiers) { + return false; + } + + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return false; + } + + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + return false; + } + + @Override + public void setX(int x) { + this.x = x; + } + + @Override + public void setY(int y) { + this.y = y; + } + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public final void forEachChild(Consumer consumer) { + throw new UnsupportedOperationException(); + } + + public boolean isFocused() { + return focused; + } + + public void setFocused(boolean focused) { + this.focused = focused; + } +} diff --git a/src/main/java/de/shiewk/widgets/WidgetSettings.java b/src/main/java/de/shiewk/widgets/WidgetSettings.java new file mode 100644 index 0000000..79ca9ef --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetSettings.java @@ -0,0 +1,104 @@ +package de.shiewk.widgets; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.shiewk.widgets.client.WidgetManager; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +import java.util.List; + +import static de.shiewk.widgets.WidgetUtils.translateToWidgetSettingsValue; +import static de.shiewk.widgets.WidgetsMod.LOGGER; + +public class WidgetSettings { + public double posX = 0; // posx * 100 = screen width + public double posY = 0; // posy * 100 = screen height + private boolean enabled = false; + private final ObjectArrayList customSettings; + + private WidgetSettings(JsonObject data, List settings){ + customSettings = new ObjectArrayList<>(settings); + if (data != null){ + final JsonElement enabled = data.get("enabled"); + this.enabled = enabled.isJsonPrimitive() && enabled.getAsJsonPrimitive().isBoolean() && enabled.getAsBoolean(); + final JsonElement x = data.get("x"); + this.posX = x.isJsonPrimitive() ? x.getAsJsonPrimitive().isNumber() ? x.getAsDouble() : 0 : 0; + final JsonElement y = data.get("y"); + this.posY = y.isJsonPrimitive() ? y.getAsJsonPrimitive().isNumber() ? y.getAsDouble() : 0 : 0; + final JsonElement s = data.get("settings"); + if (s != null && s.isJsonObject()){ + final JsonObject savedSettings = s.getAsJsonObject(); + for (WidgetSettingOption setting : this.customSettings) { + final String settingId = setting.getId(); + if (savedSettings.has(settingId)){ + try { + setting.loadState(savedSettings.get(settingId)); + } catch (Throwable e){ + LOGGER.error("Could not load setting '{}' from element {}:", settingId, savedSettings.get(settingId)); + LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + LOGGER.error(element.toString()); + } + } + } + } + } + } + } + public static WidgetSettings ofId(Identifier id, List customSettings){ + final JsonObject data = WidgetManager.loadWidget(id); + return new WidgetSettings(data, customSettings); + } + + public void setPosX(double v, int widgetWidth, int maxWidth) { + posX = MathHelper.clamp(v, 0, 100 - translateToWidgetSettingsValue(widgetWidth, maxWidth)); + } + public void setPosY(double v, int widgetHeight, int maxHeight) { + posY = MathHelper.clamp(v, 0, 100 - translateToWidgetSettingsValue(widgetHeight, maxHeight)); + } + + public boolean isEnabled(){ + return enabled; + } + + public void setEnabled(ModWidget widget, boolean enabled){ + this.enabled = enabled; + if (enabled){ + WidgetManager.enable(widget); + } else { + WidgetManager.disable(widget); + } + } + + public void toggleEnabled(ModWidget widget){ + setEnabled(widget, !enabled); + } + + public final JsonObject saveState(){ + JsonObject object = new JsonObject(); + object.addProperty("x", posX); + object.addProperty("y", posY); + object.addProperty("enabled", enabled); + JsonObject customSettings = new JsonObject(); + for (WidgetSettingOption customSetting : this.customSettings) { + customSettings.add(customSetting.getId(), customSetting.saveState()); + } + object.add("settings", customSettings); + return object; + } + + public WidgetSettingOption optionById(String id){ + for (WidgetSettingOption customSetting : customSettings) { + if (customSetting.getId().equals(id)){ + return customSetting; + } + } + return null; + } + + public ObjectArrayList getCustomSettings() { + return customSettings.clone(); + } +} diff --git a/src/main/java/de/shiewk/widgets/WidgetUtils.java b/src/main/java/de/shiewk/widgets/WidgetUtils.java new file mode 100644 index 0000000..2c9f42a --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetUtils.java @@ -0,0 +1,18 @@ +package de.shiewk.widgets; + +public class WidgetUtils { + + public static double translateToWidgetSettingsValue(double value, int max){ + return (value / max) * 100; + } + + public static double translateToScreen(double value, int max){ + return value / 100d * max; + } + + public static double computeEasing(double x) { + return 1d - Math.pow(1d - x, 3.5d); + } + + +} diff --git a/src/main/java/de/shiewk/widgets/WidgetsMod.java b/src/main/java/de/shiewk/widgets/WidgetsMod.java new file mode 100644 index 0000000..90ef9d4 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/WidgetsMod.java @@ -0,0 +1,14 @@ +package de.shiewk.widgets; + +import net.fabricmc.api.ModInitializer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WidgetsMod implements ModInitializer { + public static final Logger LOGGER = LoggerFactory.getLogger("Widgets"); + public static final String MOD_ID = "widgets"; + + @Override + public void onInitialize() { + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetManager.java b/src/main/java/de/shiewk/widgets/client/WidgetManager.java new file mode 100644 index 0000000..5439646 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetManager.java @@ -0,0 +1,98 @@ +package de.shiewk.widgets.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetsMod; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.function.Function; + +public class WidgetManager { + + static final ObjectArrayList enabled = new ObjectArrayList<>(); // for performance + private static final ObjectArrayList allWidgets = new ObjectArrayList<>(); + + public static void register(ModWidget widget){ + allWidgets.add(widget); + if (widget.getSettings().isEnabled()){ + enable(widget); + } + } + + public static ObjectArrayList getEnabledWidgets() { + return new ObjectArrayList<>(enabled); + } + + public static ObjectArrayList getAllWidgets() { + return new ObjectArrayList<>(allWidgets); + } + + public static void enable(ModWidget widget) { + enabled.add(widget); + } + + public static void disable(ModWidget widget) { + enabled.remove(widget); + } + + static Function saveFileFactory; + private static final Gson gson = new Gson(); + + public static void saveWidgets(List widgets) { + for (ModWidget widget : widgets) { + WidgetsMod.LOGGER.info("Saving widget {}", widget.getId()); + final File saveFile = saveFileFactory.apply(widget.getId()); + try { + if (saveFile.getParentFile().isDirectory() || saveFile.getParentFile().mkdirs()){ + if (saveFile.isFile() || saveFile.createNewFile()){ + try (FileWriter fw = new FileWriter(saveFile)){ + try (JsonWriter jw = new JsonWriter(fw)){ + Streams.write(widget.getSettings().saveState(), jw); + } + } + } else { + WidgetsMod.LOGGER.error("Could not create file: {}", saveFile.getPath()); + } + } else { + WidgetsMod.LOGGER.error("Could not create directory: {}", saveFile.getParentFile().getPath()); + } + } catch (IOException e){ + WidgetsMod.LOGGER.error("An I/O operation failed while saving widget {} ({})", widget, widget.getId()); + WidgetsMod.LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + WidgetsMod.LOGGER.error(String.valueOf(element)); + } + } + } + } + + public static @Nullable JsonObject loadWidget(Identifier id) { + WidgetsMod.LOGGER.info("Loading widget data of {}", id); + final File saveFile = saveFileFactory.apply(id); + if (!saveFile.isFile()){ + return null; + } + try { + try (FileReader fr = new FileReader(saveFile)){ + return gson.fromJson(fr, JsonObject.class); + } + } catch (IOException e){ + WidgetsMod.LOGGER.error("An I/O operation failed while loading widget {}", id); + WidgetsMod.LOGGER.error(e.toString()); + for (StackTraceElement element : e.getStackTrace()) { + WidgetsMod.LOGGER.error(String.valueOf(element)); + } + return null; + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java b/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java new file mode 100644 index 0000000..596aede --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetRenderer.java @@ -0,0 +1,78 @@ +package de.shiewk.widgets.client; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.client.screen.EditWidgetPositionsScreen; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.render.RenderTickCounter; +import net.minecraft.util.Util; +import net.minecraft.util.profiler.Profiler; + +import static de.shiewk.widgets.WidgetUtils.translateToScreen; + +public class WidgetRenderer implements HudRenderCallback, ClientTickEvents.StartTick, ClientLifecycleEvents.ClientStarted { + + private static MinecraftClient client; + + @Override + public void onHudRender(DrawContext drawContext, RenderTickCounter tickCounter) { + if (client.options.hudHidden) return; + if (client.currentScreen instanceof EditWidgetPositionsScreen) return; + final Profiler profiler = client.getProfiler(); + profiler.push("widgets"); + final TextRenderer textRenderer = client.textRenderer; + final long timeNano = Util.getMeasuringTimeNano(); + final int windowWidth = drawContext.getScaledWindowWidth(); + final int windowHeight = drawContext.getScaledWindowHeight(); + + final ObjectArrayList enabled = WidgetManager.enabled; + for (int i = 0, enabledSize = enabled.size(); i < enabledSize; i++) { + final ModWidget widget = enabled.get(i); + profiler.push(widget.getId().toString()); + final WidgetSettings settings = widget.getSettings(); + widget.render( + drawContext, + timeNano, + textRenderer, + (int) Math.round(Math.min(translateToScreen(settings.posX, windowWidth), windowWidth - widget.width())), + (int) Math.round(Math.min(translateToScreen(settings.posY, windowHeight), windowHeight - widget.height())) + ); + profiler.pop(); + } + profiler.pop(); + } + + @Override + public void onStartTick(MinecraftClient client) { + final Profiler profiler = (WidgetRenderer.client = client).getProfiler(); + profiler.push("widgets"); + + final ObjectArrayList enabled = WidgetManager.enabled; + for (int i = 0, enabledSize = enabled.size(); i < enabledSize; i++) { + final ModWidget widget = enabled.get(i); + profiler.push(widget.getId().toString()); + widget.tick(); + profiler.pop(); + } + + profiler.pop(); + + if (WidgetsModClient.configKeyBinding.wasPressed()){ + client.setScreen(new WidgetConfigScreen(client.currentScreen)); + } + } + + @Override + public void onClientStarted(MinecraftClient client) { + for (ModWidget widget : WidgetManager.getAllWidgets()) { + widget.onSettingsChanged(widget.getSettings()); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java b/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java new file mode 100644 index 0000000..68a0ce0 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/WidgetsModClient.java @@ -0,0 +1,62 @@ +package de.shiewk.widgets.client; + +import de.shiewk.widgets.WidgetsMod; +import de.shiewk.widgets.client.screen.WidgetConfigScreen; +import de.shiewk.widgets.widgets.*; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.client.util.InputUtil; +import net.minecraft.util.Identifier; +import org.lwjgl.glfw.GLFW; + +import java.io.File; + +public class WidgetsModClient implements ClientModInitializer { + + static KeyBinding configKeyBinding; + + + @Override + public void onInitializeClient() { + HudRenderCallback.EVENT.register(new WidgetRenderer()); + ClientTickEvents.START_CLIENT_TICK.register(new WidgetRenderer()); + ClientLifecycleEvents.CLIENT_STARTED.register(new WidgetRenderer()); + + // manage widgets keybind + configKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding( + "widgets.key.config", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_RIGHT_SHIFT, + "widgets.key.category" + )); + + // in-game /widgetsmod command + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommandManager.literal("widgetsmod").executes(ctx -> { + WidgetsMod.LOGGER.info("Ran in-game command"); + final MinecraftClient client = ctx.getSource().getClient(); + client.send(() -> client.setScreen(new WidgetConfigScreen(client.currentScreen))); + return 0; + }) + ) + ); + + WidgetManager.saveFileFactory = id -> new File(MinecraftClient.getInstance().runDirectory.getPath() + "/config/widgets/" + id.getNamespace() + "/" + id.getPath() + ".json"); + + WidgetManager.register(new FPSWidget(Identifier.of(WidgetsMod.MOD_ID, "fps"))); + WidgetManager.register(new ClockWidget(Identifier.of(WidgetsMod.MOD_ID, "clock"))); + WidgetManager.register(new CoordinatesWidget(Identifier.of(WidgetsMod.MOD_ID, "coordinates"))); + WidgetManager.register(new BandwidthWidget(Identifier.of(WidgetsMod.MOD_ID, "bandwidth"))); + WidgetManager.register(new PingWidget(Identifier.of(WidgetsMod.MOD_ID, "ping"))); + WidgetManager.register(new ServerIPWidget(Identifier.of(WidgetsMod.MOD_ID, "server_ip"))); + WidgetManager.register(new PlayerCountWidget(Identifier.of(WidgetsMod.MOD_ID, "player_count"))); + WidgetManager.register(new CPSWidget(Identifier.of(WidgetsMod.MOD_ID, "cps"))); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java b/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java new file mode 100644 index 0000000..6040c64 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/AnimatedScreen.java @@ -0,0 +1,39 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.WidgetUtils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +public abstract class AnimatedScreen extends Screen { + protected final Screen parent; + private final int animationDurationMs; + private final long creationTime = Util.getMeasuringTimeNano(); + protected AnimatedScreen(Text title, Screen parent, int animationDurationMs) { + super(title); + this.parent = parent; + this.animationDurationMs = animationDurationMs; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + double timeMs = (Util.getMeasuringTimeNano() - creationTime) / 1000000d; + final boolean shouldAnimate = timeMs < animationDurationMs; + if (shouldAnimate){ + double translation = WidgetUtils.computeEasing(timeMs / animationDurationMs) * this.width; + context.getMatrices().push(); + context.getMatrices().translate(-translation, 0, 0); + parent.render(context, (int) (mouseX + translation), mouseY, delta); + context.getMatrices().translate(this.width, 0, 0); + mouseX -= (int) translation; + } + super.render(context, mouseX, mouseY, delta); + this.renderScreenContents(context, mouseX, mouseY, delta); + if (shouldAnimate){ + context.getMatrices().pop(); + } + } + + public abstract void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta); +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java b/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java new file mode 100644 index 0000000..f047333 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/EditWidgetPositionsScreen.java @@ -0,0 +1,198 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.Dimensionable; +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.client.WidgetManager; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.function.Consumer; + +import static de.shiewk.widgets.WidgetUtils.translateToScreen; +import static de.shiewk.widgets.WidgetUtils.translateToWidgetSettingsValue; + +public class EditWidgetPositionsScreen extends AnimatedScreen { + + private record Alignment(int x, int y, int width, int height) implements Dimensionable { + + @Override + public double getX(int mx) { + return x; + } + + @Override + public double getY(int my) { + return y; + } + } + + private final Screen parent; + private final Consumer onEdit; + private boolean alignX = true; + private boolean alignY = true; + public EditWidgetPositionsScreen(Screen parent, Consumer onEdit) { + super(Text.translatable("widgets.ui.editPositions"), parent, 500); + this.parent = parent; + this.onEdit = onEdit; + } + + @Override + public void close() { + assert client != null; + client.setScreen(parent); + } + + private record AlignResult(double result, boolean isEnd){} + + private @Nullable AlignResult alignX(double x, int width, ModWidget widget){ + double endX = x + width; + int factor = alignX ? 2 : 0; + for (Dimensionable rect : this.getAlignments(widget)) { + if (rect == widget) continue; + final double nwx = rect.getX(this.width); + final double nww = rect.width(); + if (endX < nwx + factor && endX > nwx - factor){ + return new AlignResult(nwx - width, true); + } else if (x < nwx + factor && x > nwx - factor){ + return new AlignResult(nwx, false); + } else if (endX < nwx+nww + factor && endX > nwx + nww - factor){ + return new AlignResult(nwx + nww - width, true); + } else if (x < nwx+nww + factor && x > nwx + nww - factor){ + return new AlignResult(nwx + nww, false); + } + } + return null; + } + + private Iterable getAlignments(Dimensionable rel) { + ObjectArrayList alignments = new ObjectArrayList<>(); + for (ModWidget widget : WidgetManager.getEnabledWidgets()) { + alignments.add(widget); + } + alignments.add(new Alignment(2, 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2, this.height / 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(2, this.height / 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2, 2, this.width / 2 - 2, this.height / 2 - 2)); + alignments.add(new Alignment(this.width / 2 - rel.width() / 2, this.height / 2 - rel.height() / 2, rel.width(), rel.height())); + return alignments; + } + + + private @Nullable AlignResult alignY(double y, int height, ModWidget widget){ + double endY = y + height; + int factor = alignY ? 2 : 0; + for (Dimensionable rect : this.getAlignments(widget)) { + if (rect == widget) continue; + final double nwy = rect.getY(this.height); + final double nwh = rect.height(); + if (endY < nwy + factor && endY > nwy - factor){ + return new AlignResult(nwy - height, true); + } else if (y < nwy + factor && y > nwy - factor){ + return new AlignResult(nwy, false); + } else if (endY < nwy+nwh + factor && endY > nwy + nwh - factor){ + return new AlignResult(nwy + nwh - height, true); + } else if (y < nwy+nwh + factor && y > nwy + nwh - factor){ + return new AlignResult(nwy + nwh, false); + } + } + return null; + } + + private static final int SELECT_COLOR = Color.GREEN.getRGB(), ALIGN_COLOR = Color.ORANGE.getRGB(), ALIGN_DISABLED_COLOR = Color.GRAY.getRGB(); + private ModWidget selectedWidget = null; + private ModWidget hoveredWidget = null; + + @Override + public void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta) { + assert client != null; + for (ModWidget widget : WidgetManager.getEnabledWidgets()) { + final WidgetSettings settings = widget.getSettings(); + final int ww = widget.width(); + double wx = Math.min(translateToScreen(settings.posX, this.width), this.width - ww); + final int wh = widget.height(); + double wy = Math.min(translateToScreen(settings.posY, this.height), this.height - wh); + if (selectedWidget == widget){ + final AlignResult alignedX = alignX(wx, ww, widget); + if (alignedX != null){ + context.drawVerticalLine((int) (!alignedX.isEnd() ? alignedX.result() : alignedX.result() + ww), 0, this.height, alignX ? ALIGN_COLOR : ALIGN_DISABLED_COLOR); + wx = alignedX.result(); + } + final AlignResult alignedY = alignY(wy, wh, widget); + if (alignedY != null){ + context.drawHorizontalLine(0, this.width, (int) (!alignedY.isEnd() ? alignedY.result() : alignedY.result() + wh), alignY ? ALIGN_COLOR : ALIGN_DISABLED_COLOR); + wy = alignedY.result(); + } + } + if (hoveredWidget == null || hoveredWidget == widget){ + if (mouseX <= wx + ww && mouseX >= wx && mouseY <= wy + wh && mouseY >= wy){ + if (hoveredWidget == null){ + hoveredWidget = widget; + } + } else { + hoveredWidget = null; + } + } + if (selectedWidget == null ? hoveredWidget == widget : selectedWidget == widget){ + context.drawBorder((int) Math.round(wx-1), (int) Math.round(wy-1), ww+2, wh+2, SELECT_COLOR); + context.drawBorder((int) Math.round(wx), (int) Math.round(wy), ww, wh, SELECT_COLOR); + } + widget.render(context, Util.getMeasuringTimeNano(), textRenderer, (int) Math.round(wx), (int) Math.round(wy)); + } + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (button == 0 && selectedWidget != null){ + final AlignResult alignedX = alignX(translateToScreen(selectedWidget.getSettings().posX, this.width), selectedWidget.width(), selectedWidget); + if (alignedX != null){ + selectedWidget.getSettings().setPosX(translateToWidgetSettingsValue(alignedX.result(), this.width), selectedWidget.width(), this.width); + } + final AlignResult alignedY = alignY(translateToScreen(selectedWidget.getSettings().posY, this.height), selectedWidget.height(), selectedWidget); + if (alignedY != null){ + selectedWidget.getSettings().setPosY(translateToWidgetSettingsValue(alignedY.result(), this.height), selectedWidget.height(), this.height); + } + onEdit.accept(selectedWidget); + selectedWidget = null; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0 && hoveredWidget != null){ + selectedWidget = hoveredWidget; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { + if (button == 0){ + assert client != null; + final ModWidget widget = selectedWidget; + if (widget != null){ + final WidgetSettings settings = widget.getSettings(); + final int ww = widget.width(); + final int wx = (int) Math.min(translateToScreen(settings.posX, this.width), this.width - ww); + final int wh = widget.height(); + final int wy = (int) Math.min(translateToScreen(settings.posY, this.height), this.height - wh); + if (mouseX <= wx + ww + deltaX && mouseX >= wx + deltaX){ + if (mouseY <= wy + wh + deltaY && mouseY >= wy + deltaY){ + double newPosX = settings.posX + translateToWidgetSettingsValue(deltaX, this.width); + double newPosY = settings.posY + translateToWidgetSettingsValue(deltaY, this.height); + settings.setPosX(newPosX, ww, this.width); + settings.setPosY(newPosY, wh, this.height); + return true; + } + } + } + } + return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java b/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java new file mode 100644 index 0000000..d78c234 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/WidgetConfigScreen.java @@ -0,0 +1,129 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetUtils; +import de.shiewk.widgets.client.WidgetManager; +import de.shiewk.widgets.client.screen.components.WidgetListWidget; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.AxisGridWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.Consumer; + +public class WidgetConfigScreen extends Screen { + + private final Screen parent; + private final long creationTime = Util.getMeasuringTimeNano(); + private String search = ""; + private WidgetListWidget widgetList = null; + private final ArrayList widgetsEdited = new ArrayList<>(); + private final Consumer onWidgetEdit = this::changedSettings; + + private double getScreenTimeMs(){ + return (Util.getMeasuringTimeNano() - creationTime) / 1000000d; + } + + public WidgetConfigScreen(Screen parent) { + super(Text.translatable("widgets.ui.config")); + this.parent = parent; + } + + @Override + public void close() { + WidgetManager.saveWidgets(widgetsEdited); + for (ModWidget widget : widgetsEdited) { + widget.onSettingsChanged(widget.getSettings()); + } + assert client != null; + client.setScreen(parent); + } + + @Override + protected void init() { + super.init(); + AxisGridWidget agw = new AxisGridWidget(3, height - 22, width - 6, 20, AxisGridWidget.DisplayAxis.HORIZONTAL); + final TextFieldWidget searchField = new TextFieldWidget(textRenderer, this.width - 160, 20, Text.empty()); + searchField.setPlaceholder(Text.translatable("widgets.ui.search")); + searchField.setChangedListener(this::search); + if (this.widgetList != null){ + searchField.setText(this.getSearchQuery()); + } + agw.add(searchField); + agw.add(new ButtonWidget.Builder(Text.translatable("widgets.ui.editPositions"), this::switchToEditPositions).build()); + + agw.refreshPositions(); + agw.forEachChild(this::addDrawableChild); + + if (Objects.equals(search, "")){ + search(""); + } + } + + private String getSearchQuery() { + return search; + } + + public void setSearchQuery(String search) { + this.search = search; + } + + private void search(String query) { + this.setSearchQuery(query); + widgetList = new WidgetListWidget(0, 0, width-4, height-24, Text.translatable("widgets.ui.config"), client, WidgetManager.getAllWidgets().stream().filter(this::searchQueryMatches).toList(), textRenderer, this.onWidgetEdit); + } + + private void switchToEditPositions(ButtonWidget widget) { + widget.active = false; + assert client != null; + client.setScreen(new EditWidgetPositionsScreen(this, this.onWidgetEdit)); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (super.mouseClicked(mouseX, mouseY, button)){ + return true; + } else return widgetList.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)){ + return true; + } else return widgetList.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + assert client != null; + final double time = getScreenTimeMs(); + if (time < 400){ + context.getMatrices().push(); + final float v = (float) WidgetUtils.computeEasing(time / 400d); + context.getMatrices().translate(width / 2d - (width * v / 2d), height / 2d - (height * v / 2d), 0); + context.getMatrices().scale(v, v, 1); + } + super.render(context, mouseX, mouseY, delta); + if (widgetList != null){ + widgetList.render(context, mouseX, mouseY, delta); + } + if (time < 400){ + context.getMatrices().pop(); + } + } + + private boolean searchQueryMatches(ModWidget widget) { + return widget.getName().getString().contains(search) || widget.getDescription().getString().contains(search) || widget.getId().toString().contains(search); + } + + public void changedSettings(ModWidget widget) { + if (!widgetsEdited.contains(widget)){ + widgetsEdited.add(widget); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java b/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java new file mode 100644 index 0000000..0d0f592 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/WidgetSettingsScreen.java @@ -0,0 +1,50 @@ +package de.shiewk.widgets.client.screen; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.client.screen.components.WidgetSettingsEditWidget; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +public class WidgetSettingsScreen extends AnimatedScreen { + private static final Text previewText = Text.translatable("widgets.ui.preview"); + private final ModWidget widget; + private final Runnable onChange; + public WidgetSettingsScreen(Screen parent, ModWidget widget) { + super(Text.translatable("widgets.ui.widgetSettings", widget.getName()), parent, 500); + this.widget = widget; + onChange = () -> { + widget.onSettingsChanged(widget.getSettings()); + if (parent instanceof WidgetConfigScreen widgetConfigScreen){ + widgetConfigScreen.changedSettings(widget); + } + }; + } + + @Override + protected void init() { + super.init(); + addDrawableChild(new WidgetSettingsEditWidget(0, 0, this.width / 2 - 8, this.height, textRenderer, widget, this.onChange)); + } + + @Override + public void renderScreenContents(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawText(textRenderer, previewText, this.width * 3 / 4 - textRenderer.getWidth(previewText) / 2, this.height / 50, 0xffffffff, false); + widget.render(context, Util.getMeasuringTimeNano(), textRenderer, this.width * 3 / 4 - widget.width() / 2, this.height / 2 - widget.height() / 2); + } + + @Override + public void close() { + assert client != null; + client.setScreen(parent); + } + + @Override + public void tick() { + super.tick(); + if (!widget.getSettings().isEnabled()){ + widget.tick(); + } + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java new file mode 100644 index 0000000..bc3ebc6 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetListWidget.java @@ -0,0 +1,92 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.GridWidget; +import net.minecraft.client.gui.widget.ScrollableWidget; +import net.minecraft.client.gui.widget.SimplePositioningWidget; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class WidgetListWidget extends ScrollableWidget { + + private final MinecraftClient client; + private final List widgets; + private final List elements = new ArrayList<>(); + private final TextRenderer textRenderer; + private final Consumer onEdit; + + + public WidgetListWidget(int x, int y, int width, int height, Text message, MinecraftClient client, List widgets, TextRenderer textRenderer, Consumer onEdit) { + super(x, y, width, height, message); + this.client = client; + this.widgets = widgets; + this.textRenderer = textRenderer; + this.onEdit = onEdit; + init(); + } + + private void init(){ + GridWidget gw = new GridWidget(); + gw.getMainPositioner().margin(4, 4, 4, 4); + final GridWidget.Adder adder = gw.createAdder(this.width / 208); + for (ModWidget widget : widgets) { + adder.add(new WidgetWidget(0, 0, 200, 100, client, widget, textRenderer, onEdit)); + } + gw.refreshPositions(); + SimplePositioningWidget.setPos(gw, 0, 0, this.width, this.getContentsHeight(), 0.5F, 0.5F); + gw.forEachChild(w -> this.addWidget((WidgetWidget) w)); + } + + protected void addWidget(WidgetWidget drawableElement) { + this.elements.add(drawableElement); + } + + @Override + protected int getContentsHeight() { + final int rowSize = this.width / 208; + final int rows = widgets.size() % rowSize == 0 ? widgets.size() / rowSize : widgets.size() / rowSize + 1; + return 10 + (rows * 108); + } + + @Override + protected double getDeltaYPerScroll() { + return 35; + } + + @Override + protected void renderContents(DrawContext context, int mouseX, int mouseY, float delta) { + for (WidgetWidget element : elements) { + element.render(context, mouseX, (int) (mouseY + getScrollY()), delta); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseY += getScrollY(); + for (Element element : elements) { + if (element.mouseClicked(mouseX, mouseY, 0)){ + return true; + } + } + return false; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + for (ModWidget widget : widgets) { + builder.put(NarrationPart.HINT, widget.getName()); + } + } + + @Override + protected void drawBox(DrawContext context, int x, int y, int width, int height) {} +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java new file mode 100644 index 0000000..7745fa7 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetSettingsEditWidget.java @@ -0,0 +1,135 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ScrollableWidget; +import net.minecraft.text.Text; + +import java.awt.*; + +public class WidgetSettingsEditWidget extends ScrollableWidget { + private static final int COLOR_FG = Color.WHITE.getRGB(), COLOR_BG = new Color(0, 0, 0, 60).getRGB(); + private final TextRenderer textRenderer; + private final ModWidget widget; + private final Runnable onChange; + private WidgetSettingOption focus = null; + private int contentsHeight = 10; + public WidgetSettingsEditWidget(int x, int y, int width, int height, TextRenderer textRenderer, ModWidget widget, Runnable onChange) { + super(x, y, width, height, Text.empty()); + this.textRenderer = textRenderer; + this.widget = widget; + this.onChange = onChange; + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + customSetting.setFocused(false); + } + } + + @Override + protected int getContentsHeight() { + return this.contentsHeight; + } + + @Override + protected double getDeltaYPerScroll() { + return 20; + } + + @Override + protected void renderContents(DrawContext context, int mouseX, int mouseY, float delta) { + context.getMatrices().push(); + context.getMatrices().scale(2, 2, 2); + context.drawText(textRenderer, widget.getName(), this.width / 4 - textRenderer.getWidth(widget.getName()) / 2, this.height / 100, COLOR_FG, true); + context.getMatrices().pop(); + int y = textRenderer.fontHeight * 2 + this.height / 50 + 5; + for (WidgetSettingOption setting : widget.getSettings().getCustomSettings()) { + if (this.width - setting.getWidth() > textRenderer.getWidth(setting.getName()) + 20){ + setting.setX(this.getX() + this.width - setting.getWidth() - 5); + setting.setY(y); + context.drawText(textRenderer, setting.getName(), getX() + 10, y + (setting.getHeight() / 2), COLOR_FG, true); + } else { + setting.setX(this.getX() + this.width / 2 - setting.getWidth() / 2); + setting.setY(y + 9 + 5); + context.drawText(textRenderer, setting.getName(), getX() + getWidth() / 2 - textRenderer.getWidth(setting.getName()) / 2, y, COLOR_FG, true); + y += 9 + 5; + } + setting.render(context, mouseX, (int) (mouseY + getScrollY()), delta); + y += setting.getHeight(); + y += 5; + } + this.contentsHeight = y; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseY += getScrollY(); + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + if (mouseX >= customSetting.getX() && mouseX <= customSetting.getX() + customSetting.getWidth() + && mouseY >= customSetting.getY() && mouseY <= customSetting.getY() + customSetting.getHeight()){ + focus = customSetting; + customSetting.setFocused(true); + if (customSetting.mouseClicked(mouseX, mouseY + getScrollY(), button)){ + onChange.run(); + return true; + } + } + } + return super.mouseClicked(mouseX, mouseY - getScrollY(), button); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + for (WidgetSettingOption customSetting : widget.getSettings().getCustomSettings()) { + if (customSetting.mouseReleased(mouseX, mouseY + getScrollY(), button)){ + onChange.run(); + return true; + } + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (this.focus != null){ + if (this.focus.charTyped(chr, modifiers)){ + onChange.run(); + return true; + } + } + return super.charTyped(chr, modifiers); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.focus != null){ + if (this.focus.keyPressed(keyCode, scanCode, modifiers)){ + onChange.run(); + return true; + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + if (this.focus != null){ + if (this.focus.keyReleased(keyCode, scanCode, modifiers)){ + onChange.run(); + return true; + } + } + return super.keyReleased(keyCode, scanCode, modifiers); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + protected void drawBox(DrawContext context, int x, int y, int width, int height) { + context.fill(x, y, x+width, y+height, COLOR_BG); + } +} diff --git a/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java new file mode 100644 index 0000000..a696b35 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/client/screen/components/WidgetWidget.java @@ -0,0 +1,130 @@ +package de.shiewk.widgets.client.screen.components; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetUtils; +import de.shiewk.widgets.client.screen.WidgetSettingsScreen; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import net.minecraft.util.Util; + +import java.awt.*; +import java.util.Iterator; +import java.util.function.Consumer; + +public class WidgetWidget extends ClickableWidget { + + + + protected static final int COLOR_BG = new Color(0, 0, 0, 80).getRGB(), + COLOR_BG_HOVER = new Color(40, 40, 40, 80).getRGB(), + COLOR_FG = Color.WHITE.getRGB(), + COLOR_DISABLED = new Color(200, 0, 0, 200).getRGB(), + COLOR_DISABLED_HOVER = new Color(255, 0, 0, 200).getRGB(), + COLOR_ENABLED = new Color(0, 200, 0, 200).getRGB(), + COLOR_ENABLED_HOVER = new Color(0, 255, 0, 200).getRGB(); + + private final MinecraftClient client; + private final ModWidget widget; + private final TextRenderer textRenderer; + private final Consumer onEdit; + + private long toggleTime = 0; + + public WidgetWidget(int x, int y, int width, int height, MinecraftClient client, ModWidget widget, TextRenderer textRenderer, Consumer onEdit) { + super(x, y, width, height, widget.getName()); + this.client = client; + this.widget = widget; + this.textRenderer = textRenderer; + this.onEdit = onEdit; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return this.active && this.visible && mouseX >= (double)this.getX() && mouseY >= (double)this.getY() && mouseX < (double)(this.getX() + this.width) && mouseY < (double)(this.getY() + this.height - 24); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + boolean hover = this.isMouseOver(mouseX, mouseY); + boolean widgetEnabled = widget.getSettings().isEnabled(); + context.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), hover ? COLOR_BG_HOVER : COLOR_BG); + context.getMatrices().push(); + context.getMatrices().scale(2, 2, 1); + int titleSize = textRenderer.getWidth(widget.getName()); + context.drawText(textRenderer, widget.getName(), getX() / 2 + getWidth() / 4 - titleSize / 2, getY() / 2 + 4, COLOR_FG, false); + context.getMatrices().pop(); + int y = this.getY() + 12 + textRenderer.fontHeight * 2; + for (Iterator it = textRenderer.wrapLines(widget.getDescription(), this.getWidth() - 10).iterator(); it.hasNext(); y += 9) { + OrderedText t = it.next(); + context.drawText(textRenderer, t, getX() + 5 + ((getWidth() - 5) / 2) - (textRenderer.getWidth(t) / 2), y, COLOR_FG, false); + } + this.renderToggleButton(context, mouseX, mouseY, delta, widgetEnabled); + } + + @Override + protected boolean clicked(double mouseX, double mouseY) { + if (isMouseOver(mouseX, mouseY)){ + client.setScreen(new WidgetSettingsScreen(client.currentScreen, widget)); + return true; + } else if (isMouseOverToggle(mouseX, mouseY)){ + this.toggleWidget(); + return true; + } else { + return false; + } + } + + private void toggleWidget() { + widget.getSettings().toggleEnabled(widget); + toggleTime = Util.getMeasuringTimeNano(); + onEdit.accept(widget); + } + + private void renderToggleButton(DrawContext context, int mouseX, int mouseY, float delta, boolean widgetEnabled){ + boolean hoverToggle = this.isMouseOverToggle(mouseX, mouseY); + final int toggleColor; + final int toggleColorInvert; + if (hoverToggle){ + if (widgetEnabled){ + toggleColor = COLOR_ENABLED_HOVER; + toggleColorInvert = COLOR_DISABLED_HOVER; + } else { + toggleColor = COLOR_DISABLED_HOVER; + toggleColorInvert = COLOR_ENABLED_HOVER; + } + } else { + if (widgetEnabled){ + toggleColor = COLOR_ENABLED; + toggleColorInvert = COLOR_DISABLED; + } else { + toggleColor = COLOR_DISABLED; + toggleColorInvert = COLOR_ENABLED; + } + } + if (toggleTime > Util.getMeasuringTimeNano() - 250000000){ + context.fill(this.getX(), this.getY() + this.getHeight() - 24, this.getX() + this.getWidth(), this.getY() + this.getHeight(), toggleColorInvert); + context.fill(this.getX(), this.getY() + this.getHeight() - 24, (int) (WidgetUtils.computeEasing((Util.getMeasuringTimeNano() - toggleTime) / 250000000d) * this.getWidth() + this.getX()), this.getY() + this.getHeight(), toggleColor); + } else { + context.fill(this.getX(), this.getY() + this.getHeight() - 24, this.getX() + this.getWidth(), this.getY() + this.getHeight(), toggleColor); + } + context.drawCenteredTextWithShadow(textRenderer, Text.translatable(widgetEnabled ? "widgets.ui.enabled" : "widgets.ui.disabled"), this.getX() + (this.getWidth() / 2), this.getY() + this.getHeight() - 16, COLOR_FG); + } + + private boolean isMouseOverToggle(double mouseX, double mouseY) { + return this.active && this.visible && mouseX >= this.getX() && mouseY >= (this.getY() + this.height - 24) && mouseX < (this.getX() + this.width) && mouseY < (this.getY() + this.height); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public ModWidget getWidget() { + return widget; + } +} diff --git a/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java b/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java new file mode 100644 index 0000000..d943086 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/mixin/MixinMouse.java @@ -0,0 +1,27 @@ +package de.shiewk.widgets.mixin; + +import de.shiewk.widgets.widgets.CPSWidget; +import net.minecraft.client.Mouse; +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; + +@Mixin(Mouse.class) +public class MixinMouse { + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;leftButtonClicked:Z"), method = "onMouseButton") + public void onLeftClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickLeft(); + } + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;middleButtonClicked:Z"), method = "onMouseButton") + public void onMiddleClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickMiddle(); + } + + @Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/client/Mouse;rightButtonClicked:Z"), method = "onMouseButton") + public void onRightClick(long window, int button, int action, int mods, CallbackInfo ci){ + if (action == 1) CPSWidget.clickRight(); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java b/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java new file mode 100644 index 0000000..504ef85 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/BandwidthWidget.java @@ -0,0 +1,73 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; + +import java.util.List; + +public class BandwidthWidget extends BasicTextWidget { + public BandwidthWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("dynamic_color", Text.translatable("widgets.widgets.bandwidth.dynamicColor"), true) + )); + } + + private int t = 0; + private boolean dynamicColor = false; + + @Override + public void tick() { + t++; + if (t >= 20){ + t = 0; + final MultiValueDebugSampleLogImpl packetSizeLog = MinecraftClient.getInstance().getDebugHud().getPacketSizeLog(); + final int logLength = packetSizeLog.getLength(); + final int avgCompileLength = 60; + long size = 0; + for (int i = logLength-1; i > logLength-avgCompileLength; i--) { + if (i < 0) break; + size += packetSizeLog.get(i); + } + long avgBytesPerSecond = size / avgCompileLength * 20; + this.renderText = Text.of(formatByteSize(avgBytesPerSecond)); + if (this.dynamicColor){ + if (avgBytesPerSecond < 100000){ + this.textColor = 0x00ff00; + } else if (avgBytesPerSecond < 750000) { + this.textColor = 0xffff00; + } else { + this.textColor = 0xff3030; + } + } + } + } + + private String formatByteSize(long bytes) { + if (bytes > 1000) { + double mb = bytes / 100 / 10d; + return mb + " KB/s"; + } else { + return bytes + " B/s"; + } + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + this.dynamicColor = ((ToggleWidgetSetting) settings.optionById("dynamic_color")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.bandwidth"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.bandwidth.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java b/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java new file mode 100644 index 0000000..37182ad --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/BasicTextWidget.java @@ -0,0 +1,58 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettingOption; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.RGBAColorWidgetSetting; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public abstract class BasicTextWidget extends ModWidget { + protected Text renderText = Text.empty(); + + private static ObjectArrayList getCustomSettings(List otherCustomOptions) { + final ObjectArrayList list = new ObjectArrayList<>(otherCustomOptions); + list.add(new RGBAColorWidgetSetting("backgroundcolor", Text.translatable("widgets.widgets.basictext.background"), 0, 0, 0, 80)); + list.add(new RGBAColorWidgetSetting("textcolor", Text.translatable("widgets.widgets.basictext.textcolor"), 255, 255, 255, 255)); + return list; + } + protected BasicTextWidget(Identifier id, List otherCustomOptions) { + super(id, getCustomSettings(otherCustomOptions)); + } + + protected static final int + DEFAULT_WIDTH = 80, + DEFAULT_HEIGHT = 9 + 12, + DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 0, 80).getRGB(), + DEFAULT_TEXT_COLOR = new Color(255, 255 ,255, 255).getRGB(); + + protected int backgroundColor = DEFAULT_BACKGROUND_COLOR, textColor = DEFAULT_TEXT_COLOR; + + @Override + public int width() { + return DEFAULT_WIDTH; + } + + @Override + public int height() { + return DEFAULT_HEIGHT; + } + + @Override + public void render(DrawContext context, long n, TextRenderer textRenderer, int posX, int posY) { + context.fill(posX, posY, posX + width(), posY + height(), this.backgroundColor); + context.drawCenteredTextWithShadow(textRenderer, renderText, posX + (width() / 2), posY + 6, this.textColor); + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + this.backgroundColor = ((RGBAColorWidgetSetting) settings.optionById("backgroundcolor")).getColor(); + this.textColor = ((RGBAColorWidgetSetting) settings.optionById("textcolor")).getColor(); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java b/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java new file mode 100644 index 0000000..e07440b --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/CPSWidget.java @@ -0,0 +1,136 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.EnumWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class CPSWidget extends BasicTextWidget { + + public static class Click { + private int ticks = 0; + + public int tick(){ + return ticks++; + } + } + + private static boolean countLeftClicks = true; + private static boolean countMiddleClicks = true; + private static boolean countRightClicks = true; + + private static final ObjectArrayList leftClicks = new ObjectArrayList<>(); + private static final ObjectArrayList middleClicks = new ObjectArrayList<>(); + private static final ObjectArrayList rightClicks = new ObjectArrayList<>(); + + private Appearance appearance = Appearance.UNIFIED; + + public enum Appearance { + SPLIT_PIPE("pipe"), + SPLIT_SLASH("slash"), + UNIFIED("unified"); + + public final String key; + + Appearance(String key) { + this.key = key; + } + } + + public CPSWidget(Identifier id) { + super(id, List.of( + new EnumWidgetSetting<>("appearance", Text.translatable("widgets.widgets.cps.appearance"), Appearance.class, Appearance.UNIFIED, appearance -> Text.translatable("widgets.widgets.cps.appearance."+appearance.key)), + new ToggleWidgetSetting("left", Text.translatable("widgets.widgets.cps.left"), true), + new ToggleWidgetSetting("middle", Text.translatable("widgets.widgets.cps.middle"), false), + new ToggleWidgetSetting("right", Text.translatable("widgets.widgets.cps.right"), true) + )); + } + + public static void clickLeft() { + if (countLeftClicks){ + leftClicks.add(new Click()); + } + } + + public static void clickMiddle() { + if (countMiddleClicks){ + middleClicks.add(new Click()); + } + } + + public static void clickRight() { + if (countRightClicks){ + rightClicks.add(new Click()); + } + } + + @Override + public void tick() { + int left = 0; + int right = 0; + int middle = 0; + if (countLeftClicks) { + leftClicks.removeIf(click -> click.tick() >= 20); + left = leftClicks.size(); + } + if (countRightClicks) { + rightClicks.removeIf(click -> click.tick() >= 20); + right = rightClicks.size(); + } + if (countMiddleClicks) { + middleClicks.removeIf(click -> click.tick() >= 20); + middle = middleClicks.size(); + } + switch (appearance){ + case UNIFIED -> renderText = Text.literal((left + right + middle) + " CPS"); + case SPLIT_PIPE, SPLIT_SLASH -> { + final StringBuilder sb = getClickText(left, middle, right); + renderText = Text.literal(sb + " CPS"); + } + } + } + + private @NotNull StringBuilder getClickText(int left, int middle, int right) { + StringBuilder sb = new StringBuilder(); + if (countLeftClicks){ + sb.append(left); + } + if (countMiddleClicks){ + if (!sb.isEmpty()){ + sb.append(appearance == Appearance.SPLIT_PIPE ? " | " : "/"); + } + sb.append(middle); + } + if (countRightClicks){ + if (!sb.isEmpty()){ + sb.append(appearance == Appearance.SPLIT_PIPE ? " | " : "/"); + } + sb.append(right); + } + return sb; + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + countLeftClicks = ((ToggleWidgetSetting) settings.optionById("left")).getValue(); + countMiddleClicks = ((ToggleWidgetSetting) settings.optionById("middle")).getValue(); + countRightClicks = ((ToggleWidgetSetting) settings.optionById("right")).getValue(); + appearance = (Appearance) ((EnumWidgetSetting) settings.optionById("appearance")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.cps"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.cps.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java b/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java new file mode 100644 index 0000000..218e9fe --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/ClockWidget.java @@ -0,0 +1,149 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.EnumWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +public class ClockWidget extends BasicTextWidget { + + private int width = DEFAULT_WIDTH; + + public enum TimeOption { + NO_TIME("none"), + HOUR_24("24hour"), + AM_PM("am_pm"); + + public final String key; + + TimeOption(String key) { + this.key = key; + } + } + + public enum DateOption { + NO_DATE(null), + FULL_MONTH_DAY("MMMM dd"), + DAY_FULL_MONTH("dd. MMMM"), + ABB_MONTH_DAY("MMM dd"), + DAY_ABB_MONTH("dd. MMM"), + MONTH_DAY("MM/dd"), + MONTH_DAY_2("dd.MM"), + FULL_MONTH_DAY_YEAR("MMMM dd, y"), + ABB_MONTH_DAY_YEAR("MMM dd, y"), + DAY_FULL_MONTH_YEAR("dd. MMMM y"), + DAY_ABB_MONTH_YEAR("dd. MMM y"), + MONTH_DAY_YEAR("MM/dd/y"), + MONTH_DAY_YEAR_2("dd.MM.y"); + + public final String format; + + DateOption(String format) { + this.format = format; + } + + public Text getName(){ + return this.format == null ? Text.translatable("widgets.widgets.clock.dateFormat.none") : Text.of(new SimpleDateFormat(format).format(Date.from(Instant.now()))); + } + } + + public enum WeekOption { + NO_DAY_OF_WEEK(null), + ABBREVIATED("E"), + FULL("EEEE"); + + public final String format; + + WeekOption(String format) { + this.format = format; + } + + public Text getName(){ + return this.format == null ? Text.translatable("widgets.widgets.clock.weekFormat.none") : Text.of(new SimpleDateFormat(format).format(Date.from(Instant.now()))); + } + } + public ClockWidget(Identifier id) { + super(id, List.of( + new EnumWidgetSetting<>("hour_format", + Text.translatable("widgets.widgets.clock.hourFormat"), + TimeOption.class, + TimeOption.HOUR_24, + timeOption -> Text.translatable("widgets.widgets.clock.hourFormat."+timeOption.key)), + new ToggleWidgetSetting("show_seconds", + Text.translatable("widgets.widgets.clock.showSeconds"), + true), + new EnumWidgetSetting<>("date_format", + Text.translatable("widgets.widgets.clock.dateFormat"), + DateOption.class, + DateOption.NO_DATE, + DateOption::getName), + new EnumWidgetSetting<>("week_format", + Text.translatable("widgets.widgets.clock.weekFormat"), + WeekOption.class, + WeekOption.NO_DAY_OF_WEEK, + WeekOption::getName) + )); + } + + private SimpleDateFormat dateFormat = new SimpleDateFormat("hh:mm:ss aa EEEE"); + + @Override + public int width() { + return width; + } + + @Override + public void tick() { + renderText = Text.literal(dateFormat.format(Date.from(Instant.now()))); + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + String datePattern = "HH:mm:ss"; + TimeOption timeOption = (TimeOption) ((EnumWidgetSetting) settings.optionById("hour_format")).getValue(); + DateOption dateOption = (DateOption) ((EnumWidgetSetting) settings.optionById("date_format")).getValue(); + WeekOption weekOption = (WeekOption) ((EnumWidgetSetting) settings.optionById("week_format")).getValue(); + boolean showSeconds = ((ToggleWidgetSetting) settings.optionById("show_seconds")).getValue(); + if (timeOption == TimeOption.HOUR_24){ + datePattern = showSeconds ? "HH:mm:ss" : "HH:mm"; + } else if (timeOption == TimeOption.AM_PM){ + datePattern = showSeconds ? "hh:mm:ss aa" : "hh:mm aa"; + } else if (timeOption == TimeOption.NO_TIME){ + datePattern = ""; + } + if (dateOption.format != null){ + if (datePattern.isEmpty()) { + datePattern = dateOption.format + datePattern; + } else { + datePattern = dateOption.format + " " + datePattern; + } + } + if (weekOption.format != null){ + if (datePattern.isEmpty()){ + datePattern = weekOption.format + datePattern; + } else { + datePattern = weekOption.format + ", " + datePattern; + } + } + dateFormat = new SimpleDateFormat(datePattern); + width = Math.max(DEFAULT_WIDTH, MinecraftClient.getInstance().textRenderer.getWidth(dateFormat.format(Date.from(Instant.now()))) + 20); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.clock"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.clock.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java b/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java new file mode 100644 index 0000000..1935952 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/CoordinatesWidget.java @@ -0,0 +1,115 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.ModWidget; +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.RGBAColorWidgetSetting; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.awt.*; +import java.util.List; + +public class CoordinatesWidget extends ModWidget { + public CoordinatesWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("x", Text.translatable("widgets.widgets.coordinates.showX"), true), + new ToggleWidgetSetting("y", Text.translatable("widgets.widgets.coordinates.showY"), true), + new ToggleWidgetSetting("z", Text.translatable("widgets.widgets.coordinates.showZ"), true), + new RGBAColorWidgetSetting("backgroundcolor", Text.translatable("widgets.widgets.basictext.background"), 0, 0, 0, 80), + new RGBAColorWidgetSetting("textcolor", Text.translatable("widgets.widgets.basictext.textcolor"), 255, 255, 255, 255) + )); + } + + private String textX = "X", textY = "Y", textZ = "Z"; + private int txc = 0, tyc = 0, tzc = 0; + + @Override + public void render(DrawContext context, long measuringTimeNano, TextRenderer textRenderer, int posX, int posY) { + context.fill(posX, posY, posX + width(), posY + height(), this.backgroundColor); + final int PADDING = CoordinatesWidget.PADDING; + int y = PADDING; + if (showX){ + y++; + context.drawText(textRenderer, "X: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textX, posX + txc, posY + y, textColor, true); + y += textRenderer.fontHeight + 1; + } + if (showY){ + y++; + context.drawText(textRenderer, "Y: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textY, posX + tyc, posY + y, textColor, true); + y += textRenderer.fontHeight + 1; + } + if (showZ){ + y++; + context.drawText(textRenderer, "Z: ", posX + PADDING, posY + y, textColor, true); + context.drawText(textRenderer, textZ, posX + tzc, posY + y, textColor, true); + } + } + + @Override + public void tick() { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + txc = width() - textRenderer.getWidth(textX) - PADDING; + tyc = width() - textRenderer.getWidth(textY) - PADDING; + tzc = width() - textRenderer.getWidth(textZ) - PADDING; + + final ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player == null){ + textX = "?"; + textY = "?"; + textZ = "?"; + } else { + textX = String.valueOf(player.getBlockX()); + textY = String.valueOf(player.getBlockY()); + textZ = String.valueOf(player.getBlockZ()); + } + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.coordinates"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.coordinates.description"); + } + + protected static final int + WIDTH = 80, + PADDING = 6, + DEFAULT_BACKGROUND_COLOR = new Color(0, 0, 0, 80).getRGB(), + DEFAULT_TEXT_COLOR = new Color(255, 255 ,255, 255).getRGB(); + + protected int backgroundColor = DEFAULT_BACKGROUND_COLOR, textColor = DEFAULT_TEXT_COLOR; + protected boolean showX = true, showY = true, showZ = true; + + @Override + public void onSettingsChanged(WidgetSettings settings) { + this.backgroundColor = ((RGBAColorWidgetSetting) settings.optionById("backgroundcolor")).getColor(); + this.textColor = ((RGBAColorWidgetSetting) settings.optionById("textcolor")).getColor(); + this.showX = ((ToggleWidgetSetting) settings.optionById("x")).getValue(); + this.showY = ((ToggleWidgetSetting) settings.optionById("y")).getValue(); + this.showZ = ((ToggleWidgetSetting) settings.optionById("z")).getValue(); + } + + @Override + public int width() { + return WIDTH; + } + + @Override + public int height() { + int height = 2 * PADDING; + if (showX) height += 11; + if (showY) height += 11; + if (showZ) height += 11; + return height; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java b/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java new file mode 100644 index 0000000..41eeebe --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/FPSWidget.java @@ -0,0 +1,28 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class FPSWidget extends BasicTextWidget { + public FPSWidget(Identifier id) { + super(id, List.of()); + } + + @Override + public void tick() { + this.renderText = Text.literal(MinecraftClient.getInstance().getCurrentFps() + " FPS"); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.fps"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.fps.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/PingWidget.java b/src/main/java/de/shiewk/widgets/widgets/PingWidget.java new file mode 100644 index 0000000..eec5d94 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/PingWidget.java @@ -0,0 +1,69 @@ +package de.shiewk.widgets.widgets; + +import de.shiewk.widgets.WidgetSettings; +import de.shiewk.widgets.widgets.settings.ToggleWidgetSetting; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.util.profiler.MultiValueDebugSampleLogImpl; + +import java.util.List; + +public class PingWidget extends BasicTextWidget { + + long lastPingQuery = 0; + public PingWidget(Identifier id) { + super(id, List.of( + new ToggleWidgetSetting("dynamic_color", Text.translatable("widgets.widgets.ping.dynamicColor"), true) + )); + } + private boolean dynamicColor = false; + + @Override + public void tick() { + final ClientPlayNetworkHandler networkHandler = MinecraftClient.getInstance().getNetworkHandler(); + if (networkHandler != null){ + if (lastPingQuery < Util.getMeasuringTimeMs() - 5000){ + networkHandler.pingMeasurer.ping(); + lastPingQuery = Util.getMeasuringTimeMs(); + } + final MultiValueDebugSampleLogImpl pingLog = networkHandler.pingMeasurer.log; + final int logLength = pingLog.getLength(); + final int avgCompileLength = 3; + long ping = 0; + for (int i = logLength-1; i > logLength-avgCompileLength; i--) { + if (i < 0) break; + ping += pingLog.get(i); + } + long avgPing = ping / avgCompileLength; + this.renderText = Text.of(avgPing + " ms"); + if (this.dynamicColor){ + if (avgPing < 50){ + this.textColor = 0x00ff00; + } else if (avgPing < 120) { + this.textColor = 0xffff00; + } else { + this.textColor = 0xff3030; + } + } + } + } + + @Override + public void onSettingsChanged(WidgetSettings settings) { + super.onSettingsChanged(settings); + this.dynamicColor = ((ToggleWidgetSetting) settings.optionById("dynamic_color")).getValue(); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.ping"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.ping.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java b/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java new file mode 100644 index 0000000..301d95f --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/PlayerCountWidget.java @@ -0,0 +1,31 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class PlayerCountWidget extends BasicTextWidget{ + public PlayerCountWidget(Identifier id) { + super(id, List.of()); + } + + @Override + public void tick() { + final ClientPlayNetworkHandler networkHandler = MinecraftClient.getInstance().getNetworkHandler(); + String online = networkHandler == null ? "?" : String.valueOf(networkHandler.getPlayerUuids().size()); + this.renderText = Text.literal(Text.translatable("widgets.widgets.playerCount.online", online).getString()); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.playerCount"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.playerCount.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java b/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java new file mode 100644 index 0000000..9d153cd --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/ServerIPWidget.java @@ -0,0 +1,47 @@ +package de.shiewk.widgets.widgets; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.util.List; + +public class ServerIPWidget extends BasicTextWidget { + public ServerIPWidget(Identifier id) { + super(id, List.of()); + } + + private int width; + private int t = 0; + + @Override + public void tick() { + final ServerInfo serverEntry = MinecraftClient.getInstance().getCurrentServerEntry(); + if (serverEntry != null){ + this.renderText = Text.of(serverEntry.address); + } else { + this.renderText = Text.translatable("menu.singleplayer"); + } + t++; + if (t >= 20){ + t = 0; + this.width = MinecraftClient.getInstance().textRenderer.getWidth(this.renderText) + 20; + } + } + + @Override + public int width() { + return Math.max(super.width(), this.width); + } + + @Override + public Text getName() { + return Text.translatable("widgets.widgets.serverIP"); + } + + @Override + public Text getDescription() { + return Text.translatable("widgets.widgets.serverIP.description"); + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java new file mode 100644 index 0000000..04d51e4 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/EnumWidgetSetting.java @@ -0,0 +1,106 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; + +import java.awt.*; +import java.util.function.Function; + +public class EnumWidgetSetting> extends WidgetSettingOption { + + private final Class enumClass; + private T value; + private final Function enumNameGetter; + private int height = 0; + private boolean mouseClick = false; + private boolean changed = false; + + public EnumWidgetSetting(String id, Text name, Class enumClass, T defaultValue, Function enumNameGetter) { + super(id, name); + this.enumClass = enumClass; + this.value = defaultValue; + this.enumNameGetter = enumNameGetter; + } + + public T getValue(){ + return value; + } + + @Override + public JsonElement saveState() { + return new JsonPrimitive(value.name()); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isString()){ + final String name = state.getAsString(); + for (T constant : enumClass.getEnumConstants()) { + if (constant.name().equals(name)){ + this.value = constant; + } + } + } + } + + private static final int COLOR_SELECTED = new Color(0, 0x5f, 0x68, 255).getRGB(), + COLOR_UNSELECTED = new Color(0, 0, 0, 50).getRGB(), + COLOR_UNSELECTED_HOVER = new Color(80, 80, 80, 50).getRGB(), + COLOR_TEXT = new Color(255, 255, 255, 255).getRGB(); + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + int y = 0; + int nx = 5; + final int bx = getX() + getWidth(); + for (T constant : enumClass.getEnumConstants()) { + final Text name = enumNameGetter.apply(constant); + final int textRendererWidth = textRenderer.getWidth(name); + if (nx != 5 && nx + textRendererWidth + 20 > this.getWidth()){ + y += 24; + nx = 5; + } + final boolean hover = mouseX <= bx - nx && mouseX >= bx - nx - 10 - textRendererWidth && mouseY <= y + 19 + getY() && mouseY >= y + getY(); + context.fill(bx - 10 - textRendererWidth - nx, y + getY(), bx - nx, y + 19 + getY(), constant == value ? COLOR_SELECTED : hover ? COLOR_UNSELECTED_HOVER : COLOR_UNSELECTED); + context.drawText(textRenderer, name, bx - nx - 5 - textRendererWidth, y + 5 + getY(), COLOR_TEXT, true); + if (hover && mouseClick){ + this.value = constant; + this.changed = true; + } + nx += textRendererWidth + 20; + } + + mouseClick = false; + this.height = y + 34; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + mouseClick = true; + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + mouseClick = false; + boolean changed = this.changed; + this.changed = false; + return changed; + } + + @Override + public int getWidth() { + return 200; + } + + @Override + public int getHeight() { + return height; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java new file mode 100644 index 0000000..e518474 --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/RGBAColorWidgetSetting.java @@ -0,0 +1,123 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; + +import java.awt.*; + +public class RGBAColorWidgetSetting extends WidgetSettingOption { + public RGBAColorWidgetSetting(String id, Text name, int defaultR, int defaultG, int defaultB, int defaultAlpha) { + super(id, name); + this.r = defaultR; + this.g = defaultG; + this.b = defaultB; + this.a = defaultAlpha; + } + + private int r; + private int g; + private int b; + private int a; + private boolean mouseClicked = false; + private int sv = 0; + + @Override + public JsonElement saveState() { + return new JsonPrimitive(getColor()); + } + + public int getColor(){ + return new Color(r, g, b, a).getRGB(); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isNumber()){ + final Color color = new Color(state.getAsJsonPrimitive().getAsInt(), true); + this.r = color.getRed(); + this.g = color.getGreen(); + this.b = color.getBlue(); + this.a = color.getAlpha(); + } + } + + @Override + public int getWidth() { + return 28 + 127 + 7; + } + + @Override + public int getHeight() { + return 95; + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + int o = 0; + for (int i = 0; i <= 255; i+=2) { + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 5, this.getY() + 20, new Color(i, g, b, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 25, this.getY() + 40, new Color(r, i, b, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 45, this.getY() + 60, new Color(r, g, i, a).getRGB()); + context.drawVerticalLine(this.getX() + 5 + 7 + o, this.getY() + 65, this.getY() + 80, new Color(r, g, b, i).getRGB()); + o++; + } + + context.drawText(textRenderer, "R", this.getX() + 7 - textRenderer.getWidth("R"), this.getY() + 5 + 4, 0xffffff, true); + context.drawText(textRenderer, "G", this.getX() + 7 - textRenderer.getWidth("G"), this.getY() + 25 + 4, 0xffffff, true); + context.drawText(textRenderer, "B", this.getX() + 7 - textRenderer.getWidth("B"), this.getY() + 45 + 4, 0xffffff, true); + context.drawText(textRenderer, "A", this.getX() + 7 - textRenderer.getWidth("A"), this.getY() + 65 + 4, 0xffffff, true); + + context.drawText(textRenderer, String.valueOf(r), this.getX() + this.getWidth() - 19, this.getY() + 5 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(g), this.getX() + this.getWidth() - 19, this.getY() + 25 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(b), this.getX() + this.getWidth() - 19, this.getY() + 45 + 4, 0xffffff, true); + context.drawText(textRenderer, String.valueOf(a), this.getX() + this.getWidth() - 19, this.getY() + 65 + 4, 0xffffff, true); + + context.drawVerticalLine(this.getX() + 5 + 7 + r/2, this.getY() + 4, this.getY() + 21, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + g/2, this.getY() + 24, this.getY() + 41, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + b/2, this.getY() + 44, this.getY() + 61, 0xffffffff); + context.drawVerticalLine(this.getX() + 5 + 7 + a/2, this.getY() + 64, this.getY() + 81, 0xffffffff); + + if (mouseClicked){ + int col = MathHelper.clamp((mouseX - this.getX() - 5 - 7) * 2, 0, 255); + if (sv == 0){ + if (mouseY > this.getY() + 5 && mouseY < this.getY() + 20){ + sv = 1; + } else if (mouseY > this.getY() + 25 && mouseY < this.getY() + 40){ + sv = 2; + } else if (mouseY > this.getY() + 45 && mouseY < this.getY() + 60){ + sv = 3; + } else if (mouseY > this.getY() + 65 && mouseY < this.getY() + 80){ + sv = 4; + } + } + switch (sv){ + case 1 -> r = col; + case 2 -> g = col; + case 3 -> b = col; + case 4 -> a = col; + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + this.mouseClicked = true; + sv = 0; + return false; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.mouseClicked = false; + final boolean c = this.sv != 0; + this.sv = 0; + return c; + } +} diff --git a/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java b/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java new file mode 100644 index 0000000..39a5def --- /dev/null +++ b/src/main/java/de/shiewk/widgets/widgets/settings/ToggleWidgetSetting.java @@ -0,0 +1,78 @@ +package de.shiewk.widgets.widgets.settings; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import de.shiewk.widgets.WidgetSettingOption; +import de.shiewk.widgets.WidgetUtils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import net.minecraft.util.math.MathHelper; + +import java.awt.*; + +public class ToggleWidgetSetting extends WidgetSettingOption { + + private boolean value; + private long toggleTime = 0; + + public ToggleWidgetSetting(String id, Text name, boolean defaultValue) { + super(id, name); + this.value = defaultValue; + } + + public boolean getValue(){ + return value; + } + + @Override + public JsonElement saveState() { + return new JsonPrimitive(value); + } + + @Override + public void loadState(JsonElement state) { + if (state.isJsonPrimitive() && state.getAsJsonPrimitive().isBoolean()){ + this.value = state.getAsBoolean(); + } + } + + protected static final int COLOR_ENABLED = new Color(0, 255, 0, 255).getRGB(), + COLOR_ENABLED_THUMB = new Color(0, 175, 0, 255).getRGB(), + COLOR_DISABLED = new Color(255, 0, 0, 255).getRGB(), + COLOR_DISABLED_THUMB = new Color(255, 200, 200, 255).getRGB(); + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + int col = value ? COLOR_ENABLED : COLOR_DISABLED; + int thumb = value ? COLOR_ENABLED_THUMB : COLOR_DISABLED_THUMB; + int thumbLoc = (Util.getMeasuringTimeNano() - toggleTime) < 300000000f ? + (int) (getX() + MathHelper.lerp(WidgetUtils.computeEasing((Util.getMeasuringTimeNano() - toggleTime) / 300000000f), + value ? 2 : getWidth() - 4 - 12, + value ? getWidth() - 4 - 12 : 2)) + : value ? getX() + getWidth() - 4 - 12 : getX() + 4; + context.fill(getX() + 2, getY() + 2, getX() + getWidth() - 2, getY() + getHeight() - 2, col); + context.fill(thumbLoc, getY() + 4, thumbLoc + 12, getY() + getHeight() - 4, thumb); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + toggle(); + return true; + } + + public void toggle(){ + this.value = !value; + this.toggleTime = Util.getMeasuringTimeNano(); + } + + @Override + public int getWidth() { + return 50; + } + + @Override + public int getHeight() { + return 20; + } +} diff --git a/src/main/resources/assets/widgets/icon.png b/src/main/resources/assets/widgets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..65dbd588c743de9a19696eeba7ef55c109a8d48c GIT binary patch literal 46144 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

|lL31-R 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�FxjtSSd<|+-8#B7E3O-kU<2Er`{QLh! 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 zY+nkkL23!y=${loader_version}", + "fabric": "*", + "minecraft": "~${minecraft_version}" + } +} diff --git a/src/main/resources/widgets.accesswidener b/src/main/resources/widgets.accesswidener new file mode 100644 index 0000000..4e35977 --- /dev/null +++ b/src/main/resources/widgets.accesswidener @@ -0,0 +1,4 @@ +accessWidener v2 named + +accessible field net/minecraft/client/network/ClientPlayNetworkHandler pingMeasurer Lnet/minecraft/client/network/PingMeasurer; +accessible field net/minecraft/client/network/PingMeasurer log Lnet/minecraft/util/profiler/MultiValueDebugSampleLogImpl; \ No newline at end of file diff --git a/src/main/resources/widgets.mixins.json b/src/main/resources/widgets.mixins.json new file mode 100644 index 0000000..f97a76f --- /dev/null +++ b/src/main/resources/widgets.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "de.shiewk.widgets.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + ], + "client": [ + "MixinMouse" + ], + "injectors": { + "defaultRequire": 1 + } +}