diff --git a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
index 3db1ca635..81d615c4d 100644
--- a/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
+++ b/common/src/main/java/com/lambda/mixin/network/ClientPlayNetworkHandlerMixin.java
@@ -19,12 +19,14 @@
import com.lambda.event.EventFlow;
import com.lambda.event.events.InventoryEvent;
+import com.lambda.module.modules.render.NoRender;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
import net.minecraft.network.packet.s2c.play.UpdateSelectedSlotS2CPacket;
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.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
@@ -38,4 +40,9 @@ private void onUpdateSelectedSlot(UpdateSelectedSlotS2CPacket packet, CallbackIn
private void onScreenHandlerSlotUpdate(ScreenHandlerSlotUpdateS2CPacket packet, CallbackInfo ci) {
EventFlow.post(new InventoryEvent.SlotUpdate(packet.getSyncId(), packet.getRevision(), packet.getSlot(), packet.getStack()));
}
+
+ @Redirect(method = "onServerMetadata", at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;displayedUnsecureChatWarning:Z", ordinal = 0))
+ public boolean onServerMetadata(ClientPlayNetworkHandler clientPlayNetworkHandler) {
+ return NoRender.getNoChatVerificationToast();
+ }
}
diff --git a/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java
new file mode 100644
index 000000000..a1e8dd4b5
--- /dev/null
+++ b/common/src/main/java/com/lambda/mixin/render/ChatHudMixin.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.mixin.render;
+
+import com.lambda.module.modules.client.LambdaMoji;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.hud.ChatHud;
+import net.minecraft.text.OrderedText;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+@Mixin(ChatHud.class)
+public class ChatHudMixin {
+ @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/OrderedText;III)I"))
+ int redirectRenderCall(DrawContext instance, TextRenderer textRenderer, OrderedText text, int x, int y, int color) {
+ return instance.drawTextWithShadow(textRenderer, LambdaMoji.INSTANCE.parse(text, x, y, color), 0, y, 16777215 + (color << 24));
+ }
+}
diff --git a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java
index 33784a841..170b025e1 100644
--- a/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java
+++ b/common/src/main/java/com/lambda/mixin/render/ChatInputSuggestorMixin.java
@@ -17,26 +17,47 @@
package com.lambda.mixin.render;
+import com.google.common.base.Strings;
import com.lambda.command.CommandManager;
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas;
+import com.lambda.module.modules.client.LambdaMoji;
+import com.lambda.module.modules.client.RenderSettings;
import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import net.minecraft.client.gui.screen.ChatInputSuggestor;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.command.CommandSource;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
@Mixin(ChatInputSuggestor.class)
-public class ChatInputSuggestorMixin {
+public abstract class ChatInputSuggestorMixin {
@Shadow
@Final
TextFieldWidget textField;
+ @Shadow
+ private @Nullable CompletableFuture pendingSuggestions;
+
+ @Shadow
+ public abstract void show(boolean narrateFirstSuggestion);
+
@ModifyVariable(method = "refresh", at = @At(value = "STORE"), index = 3)
private boolean refreshModify(boolean showCompletions) {
return CommandManager.INSTANCE.isCommand(textField.getText());
@@ -46,4 +67,55 @@ private boolean refreshModify(boolean showCompletions) {
private CommandDispatcher refreshRedirect(ClientPlayNetworkHandler instance) {
return CommandManager.INSTANCE.currentDispatcher(textField.getText());
}
+
+ @Inject(method = "refresh", at = @At("TAIL"))
+ private void refreshEmojiSuggestion(CallbackInfo ci) {
+ if (!LambdaMoji.INSTANCE.isEnabled() ||
+ !LambdaMoji.INSTANCE.getSuggestions()) return;
+
+ String typing = textField.getText();
+
+ // Don't suggest emojis in commands
+ if (CommandManager.INSTANCE.isCommand(typing) ||
+ CommandManager.INSTANCE.isLambdaCommand(typing)) return;
+
+ int cursor = textField.getCursor();
+ String textToCursor = typing.substring(0, cursor);
+ if (textToCursor.isEmpty()) return;
+
+ // Most right index at the left of the regex expression
+ int start = neoLambda$getLastColon(textToCursor);
+ if (start == -1) return;
+
+ String emojiString = typing.substring(start + 1);
+
+ Stream results = LambdaAtlas.INSTANCE.getKeys(RenderSettings.INSTANCE.getEmojiFont())
+ .keySet().stream()
+ .filter(s -> s.startsWith(emojiString))
+ .map(s -> s + ":");
+
+ pendingSuggestions = CommandSource.suggestMatching(results, new SuggestionsBuilder(textToCursor, start + 1));
+ pendingSuggestions.thenRun(() -> {
+ if (!pendingSuggestions.isDone()) return;
+
+ show(false);
+ });
+ }
+
+ @Unique
+ private static final Pattern COLON_PATTERN = Pattern.compile("(:[a-zA-Z0-9_]+)");
+
+ @Unique
+ private int neoLambda$getLastColon(String input) {
+ if (Strings.isNullOrEmpty(input)) return -1;
+
+ int i = -1;
+ Matcher matcher = COLON_PATTERN.matcher(input);
+
+ while (matcher.find()) {
+ i = matcher.start();
+ }
+
+ return i;
+ }
}
diff --git a/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java b/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java
index 6fa80f568..cc6b0edb8 100644
--- a/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java
+++ b/common/src/main/java/com/lambda/mixin/render/ChatScreenMixin.java
@@ -17,65 +17,15 @@
package com.lambda.mixin.render;
-import com.lambda.Lambda;
import com.lambda.command.CommandManager;
-import com.lambda.graphics.renderer.gui.font.FontRenderer;
-import com.lambda.graphics.renderer.gui.font.LambdaEmoji;
-import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo;
-import com.lambda.module.modules.client.LambdaMoji;
-import com.lambda.util.math.Vec2d;
-import kotlin.Pair;
-import kotlin.ranges.IntRange;
import net.minecraft.client.gui.screen.ChatScreen;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
@Mixin(ChatScreen.class)
public abstract class ChatScreenMixin {
- @ModifyArg(method = "sendMessage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayNetworkHandler;sendChatMessage(Ljava/lang/String;)V"), index = 0)
- private String modifyChatText(String chatText) {
- if (LambdaMoji.INSTANCE.isDisabled()) return chatText;
-
- List> emojis = FontRenderer.Companion.parseEmojis(chatText, LambdaEmoji.Twemoji);
- Collections.reverse(emojis);
-
- List pushEmojis = new ArrayList<>();
- List pushPositions = new ArrayList<>();
-
- for (Pair emoji : emojis) {
- String emojiString = chatText.substring(emoji.getSecond().getStart() + 1, emoji.getSecond().getEndInclusive());
- if (LambdaEmoji.Twemoji.get(emojiString) == null)
- continue;
-
- // Because the width of a char is bigger than an emoji
- // we can simply replace the matches string by a space
- // and render it after the text
- chatText = chatText.substring(0, emoji.getSecond().getStart()) + " " + chatText.substring(emoji.getSecond().getEndInclusive() + 1);
-
- // We cannot retain the position in the future, but we can
- // assume that every time you send a message the height of
- // the position will change by the height of the glyph
- // The positions are from the top left corner of the screen
- int x = Lambda.getMc().textRenderer.getWidth(chatText.substring(0, emoji.getSecond().getStart()));
- int y = Lambda.getMc().textRenderer.fontHeight;
-
- pushEmojis.add(String.format(":%s:", emojiString));
- pushPositions.add(new Vec2d(x, y));
- }
-
- // Not optimal because it has to parse the emoji again but who cares
- LambdaMoji.INSTANCE.add(pushEmojis, pushPositions);
-
- return chatText;
- }
-
@Inject(method = "sendMessage", at = @At("HEAD"), cancellable = true)
void sendMessageInject(String chatText, boolean addToHistory, CallbackInfoReturnable cir) {
if (!CommandManager.INSTANCE.isLambdaCommand(chatText)) return;
diff --git a/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java b/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java
new file mode 100644
index 000000000..d71452858
--- /dev/null
+++ b/common/src/main/java/com/lambda/mixin/render/DrawContextMixin.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.mixin.render;
+
+import com.lambda.Lambda;
+import com.lambda.module.modules.render.MapPreview;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.tooltip.HoveredTooltipPositioner;
+import net.minecraft.client.gui.tooltip.TooltipBackgroundRenderer;
+import net.minecraft.client.gui.tooltip.TooltipComponent;
+import net.minecraft.client.gui.tooltip.TooltipPositioner;
+import net.minecraft.client.item.TooltipData;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Mixin(DrawContext.class)
+public abstract class DrawContextMixin {
+ @Shadow protected abstract void drawTooltip(TextRenderer textRenderer, List components, int x, int y, TooltipPositioner positioner);
+
+ @Inject(method = "drawTooltip(Lnet/minecraft/client/font/TextRenderer;Ljava/util/List;Ljava/util/Optional;II)V", at = @At("HEAD"), cancellable = true)
+ void drawItemTooltip(TextRenderer textRenderer, List text, Optional data, int x, int y, CallbackInfo ci) {
+ List list = text.stream().map(Text::asOrderedText).map(TooltipComponent::of).collect(Collectors.toList());
+ data.ifPresent(datax -> list.add(1, TooltipComponent.of(datax)));
+
+ var screen = (HandledScreen) Lambda.getMc().currentScreen;
+ if (screen.focusedSlot != null) {
+ var stack = screen.focusedSlot.getStack();
+ if (stack.isOf(Items.FILLED_MAP)) list.add(1, new MapPreview.MapComponent(stack));
+ }
+
+ drawTooltip(textRenderer, list, x, y, HoveredTooltipPositioner.INSTANCE);
+ ci.cancel();
+ }
+}
diff --git a/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java b/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
index d3a42ba78..3787b762b 100644
--- a/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
+++ b/common/src/main/java/com/lambda/mixin/render/SplashOverlayMixin.java
@@ -17,7 +17,7 @@
package com.lambda.mixin.render;
-import com.lambda.util.LambdaResource;
+import com.lambda.util.LambdaResourceKt;
import net.minecraft.client.gui.screen.SplashOverlay;
import net.minecraft.client.texture.ResourceTexture;
import net.minecraft.resource.DefaultResourcePack;
@@ -62,7 +62,7 @@ public LogoTextureMixin(Identifier location) {
@Redirect(method = "loadTextureData", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/DefaultResourcePack;open(Lnet/minecraft/resource/ResourceType;Lnet/minecraft/util/Identifier;)Lnet/minecraft/resource/InputSupplier;"))
InputSupplier loadTextureData(DefaultResourcePack instance, ResourceType type, Identifier id) {
- return () -> new LambdaResource("textures/lambda_banner.png").getStream();
+ return () -> LambdaResourceKt.getStream("textures/lambda_banner.png");
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/Lambda.kt b/common/src/main/kotlin/com/lambda/Lambda.kt
index dc20b593c..8e79152c2 100644
--- a/common/src/main/kotlin/com/lambda/Lambda.kt
+++ b/common/src/main/kotlin/com/lambda/Lambda.kt
@@ -20,12 +20,7 @@ package com.lambda
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.lambda.config.serializer.*
-import com.lambda.config.serializer.gui.CustomModuleWindowSerializer
-import com.lambda.config.serializer.gui.ModuleTagSerializer
-import com.lambda.config.serializer.gui.TagWindowSerializer
import com.lambda.core.Loader
-import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow
-import com.lambda.gui.impl.clickgui.windows.tag.TagWindow
import com.lambda.module.tag.ModuleTag
import com.lambda.util.KeyCode
import com.mojang.authlib.GameProfile
@@ -54,9 +49,6 @@ object Lambda {
val gson: Gson = GsonBuilder()
.setPrettyPrinting()
- .registerTypeAdapter(ModuleTag::class.java, ModuleTagSerializer)
- .registerTypeAdapter(CustomModuleWindow::class.java, CustomModuleWindowSerializer)
- .registerTypeAdapter(TagWindow::class.java, TagWindowSerializer)
.registerTypeAdapter(KeyCode::class.java, KeyCodeSerializer)
.registerTypeAdapter(Color::class.java, ColorSerializer)
.registerTypeAdapter(BlockPos::class.java, BlockPosSerializer)
diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt
index 775709c5c..3b6375842 100644
--- a/common/src/main/kotlin/com/lambda/config/Configurable.kt
+++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt
@@ -23,16 +23,14 @@ import com.google.gson.reflect.TypeToken
import com.lambda.Lambda
import com.lambda.Lambda.LOG
import com.lambda.config.settings.CharSetting
+import com.lambda.config.settings.FunctionSetting
import com.lambda.config.settings.StringSetting
import com.lambda.config.settings.collections.ListSetting
import com.lambda.config.settings.collections.MapSetting
import com.lambda.config.settings.collections.SetSetting
import com.lambda.config.settings.comparable.BooleanSetting
import com.lambda.config.settings.comparable.EnumSetting
-import com.lambda.config.settings.complex.BlockPosSetting
-import com.lambda.config.settings.complex.BlockSetting
-import com.lambda.config.settings.complex.ColorSetting
-import com.lambda.config.settings.complex.KeyBindSetting
+import com.lambda.config.settings.complex.*
import com.lambda.config.settings.numeric.*
import com.lambda.util.Communication.logError
import com.lambda.util.KeyCode
@@ -173,7 +171,6 @@ abstract class Configurable(
* @param name The unique identifier for the setting.
* @param defaultValue The default [List] value of type [T] for the setting.
* @param description A brief explanation of the setting's purpose and behavior.
- * @param hackDelegates A flag that determines whether the setting should be serialized with the default value.
* @param visibility A lambda expression that determines the visibility status of the setting.
*
* ```kotlin
@@ -204,7 +201,6 @@ abstract class Configurable(
* @param name The unique identifier for the setting.
* @param defaultValue The default [Map] value of type [K] and [V] for the setting.
* @param description A brief explanation of the setting's purpose and behavior.
- * @param hackDelegates A flag that determines whether the setting should be serialized with the default value.
* @param visibility A lambda expression that determines the visibility status of the setting.
*
* ```kotlin
@@ -424,4 +420,11 @@ abstract class Configurable(
description: String = "",
visibility: () -> Boolean = { true },
) = BlockSetting(name, defaultValue, description, visibility).register()
+
+ fun setting(
+ name: String,
+ defaultValue: () -> Unit,
+ description: String = "",
+ visibility: () -> Boolean = { true }
+ ) = FunctionSetting(name, defaultValue, description, visibility).register()
}
diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt
deleted file mode 100644
index a116d720d..000000000
--- a/common/src/main/kotlin/com/lambda/config/serializer/gui/CustomModuleWindowSerializer.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.config.serializer.gui
-
-import com.google.gson.*
-import com.lambda.gui.api.component.core.DockingRect
-import com.lambda.gui.impl.clickgui.LambdaClickGui
-import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow
-import com.lambda.module.ModuleRegistry
-import com.lambda.util.math.Vec2d
-import java.lang.reflect.Type
-
-object CustomModuleWindowSerializer : JsonSerializer, JsonDeserializer {
- override fun serialize(
- src: CustomModuleWindow?,
- typeOfSrc: Type?,
- context: JsonSerializationContext?,
- ): JsonElement = src?.let {
- JsonObject().apply {
- addProperty("title", it.title)
- add("modules", JsonArray().apply {
- it.modules.forEach {
- add(it.name)
- }
- })
- addProperty("width", it.width)
- addProperty("height", it.height)
- addProperty("isOpen", it.isOpen)
- add("position", JsonArray().apply {
- add(it.serializedPosition.x)
- add(it.serializedPosition.y)
- })
- add("docking", JsonArray().apply {
- add(it.dockingH.ordinal)
- add(it.dockingV.ordinal)
- })
- }
- } ?: JsonNull.INSTANCE
-
- override fun deserialize(
- json: JsonElement?,
- typeOfT: Type?,
- context: JsonDeserializationContext?,
- ) = json?.asJsonObject?.let {
- CustomModuleWindow(
- it["title"].asString,
- it["modules"].asJsonArray.mapNotNull { name ->
- ModuleRegistry.modules.firstOrNull { module ->
- module.name == name.asString
- }
- } as MutableList,
- LambdaClickGui
- ).apply {
- width = it["width"].asDouble
- height = it["height"].asDouble
- isOpen = it["isOpen"].asBoolean
- serializedPosition = Vec2d(
- it["position"].asJsonArray[0].asDouble,
- it["position"].asJsonArray[1].asDouble
- )
- dockingH = DockingRect.HAlign.entries[it["docking"].asJsonArray[0].asInt]
- dockingV = DockingRect.VAlign.entries[it["docking"].asJsonArray[1].asInt]
- }
- } ?: throw JsonParseException("Invalid window data")
-}
diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt
deleted file mode 100644
index f7302249e..000000000
--- a/common/src/main/kotlin/com/lambda/config/serializer/gui/ModuleTagSerializer.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.config.serializer.gui
-
-import com.google.gson.*
-import com.lambda.module.tag.ModuleTag
-import java.lang.reflect.Type
-
-object ModuleTagSerializer : JsonSerializer, JsonDeserializer {
- override fun serialize(
- src: ModuleTag?,
- typeOfSrc: Type?,
- context: JsonSerializationContext?,
- ): JsonElement =
- src?.let {
- JsonPrimitive(it.name)
- } ?: JsonNull.INSTANCE
-
- override fun deserialize(
- json: JsonElement?,
- typeOfT: Type?,
- context: JsonDeserializationContext?,
- ): ModuleTag =
- json?.asString?.let { ModuleTag(it) } ?: throw JsonParseException("Invalid module tag format")
-}
diff --git a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt b/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt
deleted file mode 100644
index 7587dce11..000000000
--- a/common/src/main/kotlin/com/lambda/config/serializer/gui/TagWindowSerializer.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.config.serializer.gui
-
-import com.google.gson.*
-import com.lambda.gui.api.component.core.DockingRect
-import com.lambda.gui.impl.clickgui.LambdaClickGui
-import com.lambda.gui.impl.clickgui.windows.tag.TagWindow
-import com.lambda.gui.impl.hudgui.LambdaHudGui
-import com.lambda.module.tag.ModuleTag
-import com.lambda.util.math.Vec2d
-import java.lang.reflect.Type
-
-object TagWindowSerializer : JsonSerializer, JsonDeserializer {
- override fun serialize(
- src: TagWindow?,
- typeOfSrc: Type?,
- context: JsonSerializationContext?,
- ): JsonElement = src?.let {
- JsonObject().apply {
- addProperty("tag", it.tag.name)
- addProperty("width", it.width)
- addProperty("height", it.height)
- addProperty("isOpen", it.isOpen)
- add("position", JsonArray().apply {
- add(it.serializedPosition.x)
- add(it.serializedPosition.y)
- })
- add("docking", JsonArray().apply {
- add(it.dockingH.ordinal)
- add(it.dockingV.ordinal)
- })
- addProperty("group", if (it.isHudWindow) "hud" else "main")
- }
- } ?: JsonNull.INSTANCE
-
- override fun deserialize(
- json: JsonElement?,
- typeOfT: Type?,
- context: JsonDeserializationContext?,
- ) = json?.asJsonObject?.let {
- val tag = ModuleTag(it["tag"].asString)
-
- val gui = when (it["group"].asString) {
- "main" -> LambdaClickGui
- "hud" -> LambdaHudGui
- else -> return@let null
- }
-
- TagWindow(tag, gui).apply {
- width = it["width"].asDouble
- height = it["height"].asDouble
- isOpen = it["isOpen"].asBoolean
- serializedPosition = Vec2d(
- it["position"].asJsonArray[0].asDouble,
- it["position"].asJsonArray[1].asDouble
- )
- dockingH = DockingRect.HAlign.entries[it["docking"].asJsonArray[0].asInt]
- dockingV = DockingRect.VAlign.entries[it["docking"].asJsonArray[1].asInt]
- }
- } ?: throw JsonParseException("Invalid window data")
-}
diff --git a/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt
new file mode 100644
index 000000000..c876314c0
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/config/settings/FunctionSetting.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.config.settings
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.reflect.TypeToken
+import com.lambda.config.AbstractSetting
+
+open class FunctionSetting(
+ override val name: String,
+ private val defaultValue: () -> T,
+ description: String,
+ visibility: () -> Boolean,
+) : AbstractSetting<() -> T>(
+ defaultValue,
+ TypeToken.get(defaultValue::class.java).type,
+ description,
+ visibility
+) {
+ override fun toJson(): JsonElement = JsonNull.INSTANCE
+ override fun loadFromJson(serialized: JsonElement) { value = defaultValue }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt
similarity index 59%
rename from common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt
rename to common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt
index 6477a2728..1fb807083 100644
--- a/common/src/main/kotlin/com/lambda/gui/api/GuiEvent.kt
+++ b/common/src/main/kotlin/com/lambda/event/events/GuiEvent.kt
@@ -15,31 +15,27 @@
* along with this program. If not, see .
*/
-package com.lambda.gui.api
+package com.lambda.event.events
import com.lambda.event.Event
import com.lambda.util.KeyCode
import com.lambda.util.Mouse
import com.lambda.util.math.Vec2d
-abstract class GuiEvent : Event {
- class Show : GuiEvent()
- class Hide : GuiEvent()
- class Tick : GuiEvent()
- class Render : GuiEvent()
+sealed class GuiEvent : Event {
+ data object Show : GuiEvent()
+ data object Hide : GuiEvent()
+ data object Tick : GuiEvent()
+ data object Update : GuiEvent()
+ data object Render : GuiEvent()
- @Deprecated("Deprecated key press event", replaceWith = ReplaceWith("com.lambda.event.events.KeyboardEvent.Press"))
class KeyPress(val key: KeyCode) : GuiEvent()
- @Deprecated("Deprecated char event", replaceWith = ReplaceWith("com.lambda.event.events.KeyboardEvent.Char"))
class CharTyped(val char: Char) : GuiEvent()
- @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Click"))
class MouseClick(val button: Mouse.Button, val action: Mouse.Action, val mouse: Vec2d) : GuiEvent()
- @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Move"))
class MouseMove(val mouse: Vec2d) : GuiEvent()
- @Deprecated("Use the new global mouse events", replaceWith = ReplaceWith("com.lambda.event.events.MouseEvent.Scroll"))
class MouseScroll(val mouse: Vec2d, val delta: Double) : GuiEvent()
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt
index e1bce2df2..e448cd798 100644
--- a/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/RenderMain.kt
@@ -20,19 +20,9 @@ package com.lambda.graphics
import com.lambda.Lambda.mc
import com.lambda.event.EventFlow.post
import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.graphics.animation.AnimationTicker
-import com.lambda.graphics.buffer.FrameBuffer
import com.lambda.graphics.gl.GlStateUtils.setupGL
import com.lambda.graphics.gl.Matrices
import com.lambda.graphics.gl.Matrices.resetMatrices
-import com.lambda.graphics.renderer.esp.global.StaticESP
-import com.lambda.graphics.renderer.esp.global.DynamicESP
-import com.lambda.graphics.shader.Shader
-import com.lambda.gui.impl.hudgui.LambdaHudGui
-import com.lambda.module.modules.client.ClickGui
import com.lambda.module.modules.client.GuiSettings
import com.lambda.util.math.Vec2d
import com.mojang.blaze3d.systems.RenderSystem.getProjectionMatrix
@@ -40,24 +30,11 @@ import org.joml.Matrix4f
object RenderMain {
val projectionMatrix = Matrix4f()
- val modelViewMatrix: Matrix4f get() = Matrices.peek()
- var screenSize = Vec2d.ZERO
-
- private val showHud get() = mc.currentScreen == null || LambdaHudGui.isOpen
+ val modelViewMatrix get() = Matrices.peek()
+ val projModel get() = Matrix4f(projectionMatrix).mul(modelViewMatrix)
- private val hudAnimation0 = with(AnimationTicker()) {
- listen {
- tick()
- }
-
- exp(0.0, 1.0, {
- if (showHud) ClickGui.closeSpeed else ClickGui.openSpeed
- }) { showHud }
- }
-
- private val frameBuffer = FrameBuffer()
- private val shader = Shader("post/cgui_animation", "renderer/pos_tex")
- private val hudAnimation by hudAnimation0
+ var screenSize = Vec2d.ZERO
+ var scaleFactor = 1.0
@JvmStatic
fun render2D() {
@@ -68,7 +45,7 @@ object RenderMain {
RenderEvent.GUI.Fixed().post()
rescale(GuiSettings.scale)
- drawHUD()
+ RenderEvent.GUI.HUD(GuiSettings.scale).post()
RenderEvent.GUI.Scaled(GuiSettings.scale).post()
}
}
@@ -80,8 +57,6 @@ object RenderMain {
setupGL {
RenderEvent.World().post()
- StaticESP.render()
- DynamicESP.render()
}
}
@@ -93,21 +68,8 @@ object RenderMain {
val scaledHeight = height / factor
screenSize = Vec2d(scaledWidth, scaledHeight)
- projectionMatrix.setOrtho(0f, scaledWidth.toFloat(), scaledHeight.toFloat(), 0f, 1000f, 21000f)
- }
-
- private fun drawHUD() {
- if (hudAnimation < 0.001) return
-
- if (hudAnimation > 0.999) {
- RenderEvent.GUI.HUD(GuiSettings.scale).post()
- return
- }
+ scaleFactor = factor
- frameBuffer.write {
- RenderEvent.GUI.HUD(GuiSettings.scale).post()
- }.read(shader) {
- it["u_Progress"] = hudAnimation
- }
+ projectionMatrix.setOrtho(0f, scaledWidth.toFloat(), scaledHeight.toFloat(), 0f, 1000f, 21000f)
}
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt
index f9a13eea9..01755d7ea 100644
--- a/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/animation/Animation.kt
@@ -27,11 +27,11 @@ class Animation(initialValue: Double, val update: (Double) -> Double) {
private var prevValue = initialValue
private var currValue = initialValue
- operator fun getValue(thisRef: Any?, property: KProperty<*>) =
- lerp(mc.partialTicks, prevValue, currValue)
-
+ operator fun getValue(thisRef: Any?, property: KProperty<*>) = value()
operator fun setValue(thisRef: Any?, property: KProperty<*>, valueIn: Double) = setValue(valueIn)
+ fun value(): Double = lerp(mc.partialTicks, prevValue, currValue)
+
fun setValue(valueIn: Double) {
prevValue = valueIn
currValue = valueIn
@@ -52,16 +52,16 @@ class Animation(initialValue: Double, val update: (Double) -> Double) {
fun AnimationTicker.exp(min: Double, max: Double, speed: Double, flag: () -> Boolean) =
exp({ min }, { max }, { speed }, flag)
- fun AnimationTicker.exp(target: () -> Double, speed: Double) =
+ fun AnimationTicker.exp(speed: Double, target: () -> Double) =
exp(target, target, { speed }, { true })
fun AnimationTicker.exp(min: () -> Double, max: () -> Double, speed: () -> Double, flag: () -> Boolean) =
Animation(min()) {
- val min = min()
- val max = max()
- val target = if (flag()) max else min
+ val minVal = min()
+ val maxVal = max()
+ val target = if (flag()) maxVal else minVal
- if (abs(target - it) < CLAMP * abs(max - min)) target
+ if (abs(target - it) < CLAMP * abs(maxVal - minVal)) target
else lerp(speed(), it, target)
}.apply(::register)
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt
similarity index 80%
rename from common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt
rename to common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt
index 9aaeb7a00..7af4ae588 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/IBuffer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/Buffer.kt
@@ -20,11 +20,10 @@ package com.lambda.graphics.buffer
import com.lambda.graphics.gl.bufferBound
import com.lambda.graphics.gl.bufferUsageValid
import com.lambda.graphics.gl.bufferValid
-import org.lwjgl.opengl.GL30C.*
-import org.lwjgl.opengl.GL44.glBufferStorage
+import org.lwjgl.opengl.GL44.*
import java.nio.ByteBuffer
-interface IBuffer {
+abstract class Buffer(
/**
* Specifies how many buffer must be used
*
@@ -40,8 +39,13 @@ interface IBuffer {
*
* Triple buffering helps maintain smoother frame rates, but if your app runs faster than the monitor's refresh rate, it offers little benefit as you eventually still wait for vblank synchronization.
*/
- val buffers: Int
+ val buffers: Int = 1,
+ /**
+ * Edge case to handle vertex arrays
+ */
+ val isVertexArray: Boolean = false,
+) {
/**
* Specifies how the buffers are used
*
@@ -57,7 +61,7 @@ interface IBuffer {
* | GL_DYNAMIC_READ | Data is modified repeatedly and used many times for reading. |
* | GL_DYNAMIC_COPY | Data is modified repeatedly and used many times for copying. |
*/
- val usage: Int
+ abstract val usage: Int
/**
* Specifies the target to which the buffer object is bound which must be one
@@ -80,7 +84,7 @@ interface IBuffer {
* | GL_TRANSFORM_FEEDBACK_BUFFER | Transform feedback buffer |
* | GL_UNIFORM_BUFFER | Uniform block storage |
*/
- val target: Int
+ abstract val target: Int
/**
* Specifies a combination of access flags indicating the desired
@@ -97,27 +101,32 @@ interface IBuffer {
* | GL_MAP_FLUSH_EXPLICIT_BIT | Requires explicit flushing of modified sub-ranges. | Only with GL_MAP_WRITE_BIT. Data may be undefined if skipped. |
* | GL_MAP_UNSYNCHRONIZED_BIT | Skips synchronization before mapping. | May cause data corruption if regions overlap. |
*/
- val access: Int
+ abstract val access: Int
/**
* Index of the current buffer
*/
- var index: Int
+ var index: Int = 0; private set
/**
* List of all the buffers
*/
- val bufferIds: IntArray
+ private val bufferIds = IntArray(buffers)
/**
* Binds the buffer id to the [target]
*/
- fun bind(id: Int) = glBindBuffer(target, id)
+ open fun bind(id: Int) = glBindBuffer(target, id)
/**
* Binds current the buffer [index] to the [target]
*/
- fun bind() = bind(bufferIds[index])
+ fun bind() = bind(bufferAt(index))
+
+ /**
+ * Returns the id of the buffer based on the index
+ */
+ fun bufferAt(index: Int) = bufferIds[index]
/**
* Swaps the buffer [index] if [buffers] is greater than 1
@@ -130,7 +139,7 @@ interface IBuffer {
* Update the current buffer without re-allocating
* Alternative to [map]
*/
- fun update(
+ open fun update(
data: ByteBuffer,
offset: Long,
): Throwable? {
@@ -151,91 +160,111 @@ interface IBuffer {
*
* @param data The data to put in the new allocated buffer
*/
- fun allocate(data: ByteBuffer): Throwable? {
+ open fun allocate(data: ByteBuffer): Throwable? {
if (!bufferValid(target, access))
return IllegalArgumentException("Target is not valid. Refer to the table in the documentation")
if (!bufferUsageValid(usage))
return IllegalArgumentException("Buffer usage is invalid")
- bind()
- glBufferData(target, data, usage)
+ repeat(buffers) {
+ bind()
+ glBufferData(target, data, usage)
+ swap()
+ }
+
bind(0)
return null
}
/**
- * Grows the backing buffers
+ * Allocates memory for each backing buffer using the specified size
* This function handles the buffer binding
*
* @param size The size of the new buffer
+ * @return An [IllegalArgumentException] if validation fails; null if the allocation succeeds
*/
- fun allocate(size: Long): Throwable? {
+ open fun allocate(size: Long): Throwable? {
if (!bufferValid(target, access))
return IllegalArgumentException("Target is not valid. Refer to the table in the documentation")
if (!bufferUsageValid(usage))
return IllegalArgumentException("Buffer usage is invalid")
- bind()
- glBufferData(target, size.coerceAtLeast(0), usage)
+ repeat(buffers) {
+ bind()
+ glBufferData(target, size.coerceAtLeast(0), usage)
+ swap()
+ }
+
bind(0)
return null
}
/**
- * Create a new buffer storage
+ * Allocates new storage for the OpenGL buffer using the provided data
* This function cannot be called twice for the same buffer
* This function handles the buffer binding
+ *
+ * @return [IllegalArgumentException] for an invalid target or usage; null if storage allocation is successful
*/
- fun storage(data: ByteBuffer): Throwable? {
+ open fun storage(data: ByteBuffer): Throwable? {
if (!bufferValid(target, access))
return IllegalArgumentException("Target is not valid. Refer to the table in the documentation")
if (!bufferUsageValid(usage))
return IllegalArgumentException("Buffer usage is invalid")
- bind()
- glBufferStorage(target, data, usage)
+ repeat(buffers) {
+ bind()
+ glBufferStorage(target, data, access or GL_DYNAMIC_STORAGE_BIT)
+ swap()
+ }
+
bind(0)
return null
}
/**
- * Create a new buffer storage
+ * Allocates storage for the buffer object
* This function cannot be called twice for the same buffer
* This function handles the buffer binding
*
* @param size The size of the storage buffer
+ * @return [IllegalArgumentException] if the target or usage is invalid; null if storage allocation succeeds
*/
- fun storage(size: Long): Throwable? {
+ open fun storage(size: Long): Throwable? {
if (!bufferValid(target, access))
return IllegalArgumentException("Target is not valid. Refer to the table in the documentation")
if (!bufferUsageValid(usage))
return IllegalArgumentException("Buffer usage is invalid")
- bind()
- glBufferStorage(target, size.coerceAtLeast(0), usage)
+ repeat(buffers) {
+ bind()
+ glBufferStorage(target, size.coerceAtLeast(0), access or GL_DYNAMIC_STORAGE_BIT)
+ swap()
+ }
+
bind(0)
return null
}
/**
- * Maps all or part of a buffer object's data store into the client's address space
+ * Maps a specified region of the buffer's data store into client memory, processes it using the provided lambda, and then unmaps the buffer
*
- * @param offset Specifies the starting offset within the buffer of the range to be mapped.
* @param size Specifies the length of the range to be mapped.
+ * @param offset Specifies the starting offset within the buffer of the range to be mapped.
* @param block Lambda scope with the mapped buffer passed in
- * @return Error encountered during the mapping process
+ * @return [IllegalArgumentException] if there were errors during the validation, mapping or unmapping, null otherwise
*/
- fun map(
- offset: Long,
+ open fun map(
size: Long,
+ offset: Long,
block: (ByteBuffer) -> Unit
): Throwable? {
if (
@@ -251,7 +280,7 @@ interface IBuffer {
if (
offset + size > glGetBufferParameteri(target, GL_BUFFER_SIZE)
- ) return IllegalArgumentException("Out of bound mapping: $offset + $size > ${glGetBufferParameteri(target, GL_BUFFER_SIZE)}")
+ ) return IllegalArgumentException("Out of bound (is the buffer initialized?) $size + $offset > ${glGetBufferParameteri(target, GL_BUFFER_SIZE)}")
if (
glGetBufferParameteri(target, GL_BUFFER_MAPPED)
@@ -286,21 +315,21 @@ interface IBuffer {
}
/**
- * Sets the given data into the client mapped memory and executes the provided processing function to manage data transfer.
+ * Uploads the specified data to the buffer starting at the given offset
*
- * @param data Data to set in memory
- * @param offset The starting offset within the buffer of the range to be mapped
- * @return Error encountered during the mapping process
- */
- fun upload(data: ByteArray, offset: Long): Throwable? =
- upload(ByteBuffer.wrap(data), offset)
-
- /**
- * Sets the given data into the client mapped memory and executes the provided processing function to manage data transfer.
+ * This abstract function should be implemented to perform the actual data transfer into the buffer
*
* @param data Data to set in memory
* @param offset The starting offset within the buffer of the range to be mapped
* @return Error encountered during the mapping process
*/
- fun upload(data: ByteBuffer, offset: Long): Throwable?
+ abstract fun upload(data: ByteBuffer, offset: Long): Throwable?
+
+ init {
+ // Special edge case for vertex arrays
+ check(buffers > 0) { "Cannot generate less than one buffer" }
+
+ if (isVertexArray) glGenVertexArrays(bufferIds) // If there are more than 1 buffer you should expect undefined behavior, this is not the way to do it
+ else glGenBuffers(bufferIds)
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt
deleted file mode 100644
index dce732b33..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/FrameBuffer.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.buffer
-
-import com.lambda.Lambda.mc
-import com.lambda.graphics.RenderMain
-import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.gl.GlStateUtils.withBlendFunc
-import com.lambda.graphics.shader.Shader
-import com.lambda.graphics.texture.TextureUtils.bindTexture
-import com.lambda.graphics.texture.TextureUtils.setupTexture
-import com.lambda.util.math.Vec2d
-import org.lwjgl.opengl.GL30.*
-import java.nio.IntBuffer
-
-class FrameBuffer(private val depth: Boolean = false) {
- private val fbo = glGenFramebuffers()
-
- private val colorAttachment = glGenTextures()
- private val depthAttachment by lazy(::glGenTextures)
-
- private val clearMask = if (!depth) GL_COLOR_BUFFER_BIT
- else GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT
-
- private var width = 0
- private var height = 0
-
- fun write(block: () -> Unit): FrameBuffer {
- val prev = lastFrameBuffer ?: mc.framebuffer.fbo
-
- glBindFramebuffer(GL_FRAMEBUFFER, fbo)
- lastFrameBuffer = fbo
-
- update()
- withBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, block)
-
- lastFrameBuffer = prev
- glBindFramebuffer(GL_FRAMEBUFFER, prev)
- return this
- }
-
- fun read(
- shader: Shader,
- pos1: Vec2d = Vec2d.ZERO,
- pos2: Vec2d = RenderMain.screenSize,
- shaderBlock: (Shader) -> Unit = {}
- ): FrameBuffer {
- bindColorTexture()
-
- shader.use()
- shaderBlock(shader)
-
- pipeline.use {
- grow(4)
-
- val uv1 = pos1 / RenderMain.screenSize
- val uv2 = pos2 / RenderMain.screenSize
-
- putQuad(
- vec2(pos1.x, pos1.y).vec2(uv1.x, 1.0 - uv1.y).end(),
- vec2(pos1.x, pos2.y).vec2(uv1.x, 1.0 - uv2.y).end(),
- vec2(pos2.x, pos2.y).vec2(uv2.x, 1.0 - uv2.y).end(),
- vec2(pos2.x, pos1.y).vec2(uv2.x, 1.0 - uv1.y).end()
- )
- }
-
- withBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) {
- pipeline.upload()
- pipeline.render()
- pipeline.clear()
- }
-
- return this
- }
-
- fun bindColorTexture(slot: Int = 0): FrameBuffer {
- bindTexture(colorAttachment, slot)
- return this
- }
-
- fun bindDepthTexture(slot: Int = 0): FrameBuffer {
- check(depth) {
- "Cannot bind depth texture of a non-depth framebuffer"
- }
-
- bindTexture(depthAttachment, slot)
- return this
- }
-
- private fun update() {
- val widthIn = mc.window.framebufferWidth
- val heightIn = mc.window.framebufferHeight
-
- if (width != widthIn || height != heightIn) {
- width = widthIn
- height = heightIn
-
- setupBufferTexture(colorAttachment)
- glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as IntBuffer?)
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorAttachment, 0)
-
- if (depth) {
- setupBufferTexture(depthAttachment)
- glTexImage2D(
- GL_TEXTURE_2D, // Target
- 0, // LOD Level
- GL_DEPTH_COMPONENT32F, // Internal Format
- width, // Width
- height, // Height
- 0, // Border (must be zero)
- GL_DEPTH_COMPONENT, // Format
- GL_FLOAT, // Type
- null as IntBuffer? // Pointer to data
- )
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthAttachment, 0)
- }
-
- glClearColor(0f, 0f, 0f, 0f)
- glClearDepth(1.0)
- }
-
- glClear(clearMask)
- }
-
- private fun setupBufferTexture(id: Int) {
- bindTexture(id)
- setupTexture(GL_NEAREST, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
- }
-
- companion object {
- private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV)
- private var lastFrameBuffer: Int? = null
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt
index 3fb28a33b..3bb15197e 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/IRenderContext.kt
@@ -38,6 +38,12 @@ interface IRenderContext {
fun upload()
fun clear()
+ fun immediateDraw() {
+ upload()
+ render()
+ clear()
+ }
+
fun grow(amount: Int)
fun use(block: IRenderContext.() -> Unit) {
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt
index ef886c56b..c8be117bf 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/VertexPipeline.kt
@@ -32,19 +32,21 @@ import com.lambda.graphics.gl.Memory.vector2f
import com.lambda.graphics.gl.Memory.vector3f
import com.lambda.graphics.gl.kibibyte
import org.joml.Vector4d
+import org.lwjgl.opengl.GL15C
import org.lwjgl.opengl.GL20C.*
import java.awt.Color
class VertexPipeline(
private val mode: VertexMode,
attributes: VertexAttrib.Group,
+ usage: Int = GL_DYNAMIC_DRAW
) : IRenderContext {
private val stride = attributes.stride
private val size = stride * mode.indicesCount
private val vao = VertexArray()
- private val vbo = VertexBuffer(mode, attributes)
- private val ebo = ElementBuffer(mode)
+ private val vbo = VertexBuffer(mode, attributes, usage)
+ private val ebo = ElementBuffer(mode, usage)
private var vertices = byteBuffer(size * 1.kibibyte)
private var verticesPointer = address(vertices)
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt
new file mode 100644
index 000000000..9126de77e
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/CachedFrame.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.buffer.frame
+
+import com.lambda.Lambda.mc
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.gl.Matrices
+import org.joml.Matrix4f
+import org.lwjgl.opengl.GL11C.glViewport
+
+/**
+ * A class that handles a cached frame, encapsulating a framebuffer with a specified width and height.
+ * It provides methods for binding the framebuffer texture and writing to the framebuffer with custom rendering operations.
+ *
+ * @param width The width of the framebuffer.
+ * @param height The height of the framebuffer.
+ */
+class CachedFrame(val width: Int, val height: Int) {
+
+ // The framebuffer associated with this cached frame
+ private val frameBuffer = FrameBuffer(width, height)
+
+ /**
+ * Binds the color texture of the framebuffer to a specified texture slot.
+ *
+ * @param slot The texture slot to bind the color texture to. Defaults to slot 0.
+ */
+ fun bind(slot: Int = 0) = frameBuffer.bindColorTexture(slot)
+
+ /**
+ * Executes custom drawing operations on the framebuffer.
+ *
+ * The method temporarily modifies the view and projection matrices, the viewport,
+ * and then restores them after the block is executed.
+ *
+ * @param block A block of code that performs custom drawing operations on the framebuffer.
+ */
+ fun write(block: () -> Unit): CachedFrame {
+ frameBuffer.write {
+ // Save the current viewmodel matrix
+ Matrices.push()
+ // Set the viewmodel matrix to translate the scene away
+ Matrices.peek().set(Matrix4f().translate(0f, 0f, -3000f))
+
+ // Save the previous projection matrix and set a custom orthographic projection
+ val prevProj = Matrix4f(RenderMain.projectionMatrix)
+ RenderMain.projectionMatrix.setOrtho(0f, width.toFloat(), height.toFloat(), 0f, 1000f, 21000f)
+
+ // Resize the viewport to match the framebuffer's dimensions
+ glViewport(0, 0, width, height)
+
+ // Execute the drawing operations defined in the block
+ block()
+
+ // Restore the previous viewport dimensions
+ glViewport(0, 0, mc.framebuffer.viewportWidth, mc.framebuffer.viewportHeight)
+
+ // Restore the previous projection matrix
+ RenderMain.projectionMatrix.set(prevProj)
+
+ // Restore the previous viewmodel matrix
+ Matrices.pop()
+ }
+
+ return this
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt
new file mode 100644
index 000000000..d340b524c
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/FrameBuffer.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.buffer.frame
+
+import com.lambda.Lambda.mc
+import com.lambda.graphics.RenderMain.projectionMatrix
+import com.lambda.graphics.buffer.VertexPipeline
+import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
+import com.lambda.graphics.buffer.vertex.attributes.VertexMode
+import com.lambda.graphics.gl.Matrices
+import com.lambda.graphics.texture.TextureUtils
+import org.joml.Matrix4f
+import org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE
+import org.lwjgl.opengl.GL30C.*
+import java.nio.IntBuffer
+
+open class FrameBuffer(
+ protected var width: Int = 1,
+ protected var height: Int = 1,
+ private val depth: Boolean = false
+) {
+ private val fbo = glGenFramebuffers()
+
+ private val colorAttachment = glGenTextures()
+ private val depthAttachment by lazy(::glGenTextures)
+
+ private val clearMask = if (!depth) GL_COLOR_BUFFER_BIT
+ else GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT
+
+ private var lastWidth = -1
+ private var lastHeight = -1
+
+ open fun write(block: () -> Unit): FrameBuffer {
+ val prev = lastFrameBuffer ?: mc.framebuffer.fbo
+
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo)
+ lastFrameBuffer = fbo
+
+ update()
+
+ block()
+
+ lastFrameBuffer = prev
+ glBindFramebuffer(GL_FRAMEBUFFER, prev)
+ return this
+ }
+
+ private fun update() {
+ if (width == lastWidth && height == lastWidth) {
+ glClear(clearMask)
+ return
+ }
+
+ lastWidth = width
+ lastHeight = height
+
+ setupBufferTexture(colorAttachment)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, null as IntBuffer?)
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorAttachment, 0)
+
+ if (depth) {
+ setupBufferTexture(depthAttachment)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, null as IntBuffer?)
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthAttachment, 0)
+ }
+
+ glClearColor(0f, 0f, 0f, 0f)
+ glClearDepth(1.0)
+
+ glClear(clearMask)
+
+ val fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER)
+
+ check(fboStatus == GL_FRAMEBUFFER_COMPLETE) {
+ "Framebuffer not complete: $fboStatus"
+ }
+ }
+
+ open fun bindColorTexture(slot: Int = 0): FrameBuffer {
+ TextureUtils.bindTexture(colorAttachment, slot)
+ return this
+ }
+
+ open fun bindDepthTexture(slot: Int = 0): FrameBuffer {
+ check(depth) {
+ "Cannot bind depth texture of a non-depth framebuffer"
+ }
+
+ TextureUtils.bindTexture(depthAttachment, slot)
+ return this
+ }
+
+ companion object {
+ val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV)
+ private var lastFrameBuffer: Int? = null
+
+ private fun setupBufferTexture(id: Int) {
+ TextureUtils.bindTexture(id)
+ TextureUtils.setupTexture(GL_LINEAR, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt
new file mode 100644
index 000000000..466473a81
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/frame/ScreenFrameBuffer.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.buffer.frame
+
+import com.lambda.Lambda.mc
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.gl.GlStateUtils.withBlendFunc
+import com.lambda.graphics.shader.Shader
+import com.lambda.util.math.Vec2d
+import org.lwjgl.opengl.GL11C.*
+
+class ScreenFrameBuffer(depth: Boolean = false) : FrameBuffer(depth = depth) {
+ fun read(
+ shader: Shader,
+ pos1: Vec2d = Vec2d.ZERO,
+ pos2: Vec2d = RenderMain.screenSize,
+ shaderBlock: (Shader) -> Unit = {}
+ ): ScreenFrameBuffer {
+ bindColorTexture()
+
+ shader.use()
+ shaderBlock(shader)
+
+ pipeline.use {
+ grow(4)
+
+ val uv1 = pos1 / RenderMain.screenSize
+ val uv2 = pos2 / RenderMain.screenSize
+
+ putQuad(
+ vec2(pos1.x, pos1.y).vec2(uv1.x, 1.0 - uv1.y).end(),
+ vec2(pos1.x, pos2.y).vec2(uv1.x, 1.0 - uv2.y).end(),
+ vec2(pos2.x, pos2.y).vec2(uv2.x, 1.0 - uv2.y).end(),
+ vec2(pos2.x, pos1.y).vec2(uv2.x, 1.0 - uv1.y).end()
+ )
+ }
+
+ withBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA) {
+ pipeline.upload()
+ pipeline.render()
+ pipeline.clear()
+ }
+
+ return this
+ }
+
+ override fun write(block: () -> Unit): ScreenFrameBuffer {
+ width = mc.window.framebufferWidth
+ height = mc.window.framebufferHeight
+
+ return super.write {
+ withBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, block)
+ } as ScreenFrameBuffer
+ }
+
+ override fun bindColorTexture(slot: Int) =
+ super.bindColorTexture(slot) as ScreenFrameBuffer
+
+ override fun bindDepthTexture(slot: Int) =
+ super.bindDepthTexture(slot) as ScreenFrameBuffer
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt
index ef8f7495d..c8e590125 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/pixel/PixelBuffer.kt
@@ -17,110 +17,81 @@
package com.lambda.graphics.buffer.pixel
-import com.lambda.graphics.buffer.IBuffer
-import com.lambda.graphics.gl.padding
+import com.lambda.graphics.buffer.Buffer
import com.lambda.graphics.gl.putTo
import com.lambda.graphics.texture.Texture
+import com.lambda.util.math.MathUtils.toInt
import org.lwjgl.opengl.GL45C.*
import java.nio.ByteBuffer
/**
* Represents a Pixel Buffer Object (PBO) that facilitates asynchronous data transfer to the GPU.
- * This class manages the creation, usage, and cleanup of PBOs and provides methods to upload (map) data efficiently.
*
- * **Process**:
* Every function that performs a pixel transfer operation can use buffer objects instead of client memory.
* Functions that perform an upload operation, a pixel unpack, will use the buffer object bound to the target GL_PIXEL_UNPACK_BUFFER.
* If a buffer is bound, then the pointer value that those functions take is not a pointer, but an offset from the beginning of that buffer.
*
- * @property width The width of the texture
- * @property height The height of the texture
- * @property texture The [Texture] instance
- * @property format The image format that will be uploaded
+ * @property texture The [Texture] instance to use
+ * @property asynchronous Whether to use 2 buffers or not
+ * @property bufferMapping Whether to map a block in memory to upload or not
*
- * @see Pixel Buffer Object
+ * @see Reference
*/
class PixelBuffer(
- private val width: Int,
- private val height: Int,
private val texture: Texture,
- private val format: Int,
-) : IBuffer {
- override val buffers: Int = 2
+ private val asynchronous: Boolean = false,
+ private val bufferMapping: Boolean = false,
+) : Buffer(buffers = asynchronous.toInt() + 1) {
override val usage: Int = GL_STATIC_DRAW
override val target: Int = GL_PIXEL_UNPACK_BUFFER
- override val access: Int = GL_MAP_WRITE_BIT or GL_MAP_COHERENT_BIT
- override var index = 0
- override val bufferIds = IntArray(buffers).apply { glGenBuffers(this) }
+ override val access: Int = GL_MAP_WRITE_BIT
- private val channels = channelMapping[format] ?: throw IllegalArgumentException("Image format unsupported")
- private val internalFormat = reverseChannelMapping[channels] ?: throw IllegalArgumentException("Image internal format unsupported")
- private val size = width * height * channels * 1L
+ private val channels = channelMapping[texture.format] ?: throw IllegalArgumentException("Invalid image format, expected OpenGL format, got ${texture.format} instead")
+ private val size = texture.width * texture.height * channels * 1L
override fun upload(
data: ByteBuffer,
offset: Long,
): Throwable? {
- // Bind PBO to unpack the data into the texture
bind()
-
- // Bind the texture and PBO
glBindTexture(GL_TEXTURE_2D, texture.id)
// Copy pixels from PBO to texture object
// Use offset instead of pointer
glTexSubImage2D(
GL_TEXTURE_2D, // Target
- 0, // Mipmap level
- 0, 0, // x and y offset
- width, height, // width and height of the texture (set to your size)
- format, // Format (depends on your data)
+ 0, // Mipmap level
+ 0, 0, // x and y offset
+ texture.width, // Width of the texture
+ texture.height, // Height of the texture
+ texture.format, // Format of your texture (depends on your data)
GL_UNSIGNED_BYTE, // Type (depends on your data)
- 0, // PBO offset (for asynchronous transfer)
+ 0, // PBO offset (for asynchronous transfer)
)
- // Unbind the texture
- glBindTexture(GL_TEXTURE_2D, 0)
-
- // Swap the buffer
swap()
-
- // Bind PBO to update pixel source
bind()
- // Map the buffer into the client's memory
- val error = map(offset, size, data::putTo)
+ val error =
+ if (bufferMapping) map(size, offset, data::putTo)
+ else update(data, offset)
- // Unbind
bind(0)
return error
}
init {
- // Bind the texture
- glBindTexture(GL_TEXTURE_2D, texture.id)
-
- // Calculate memory padding in the case we are using tightly
- // packed data in order to save memory and satisfy the computer's
- // architecture memory alignment
- // https://en.wikipedia.org/wiki/Data_structure_alignment
- // In this case we calculate the padding and subtract this to 4
- // in order to tell the padding size
- glPixelStorei(GL_UNPACK_ALIGNMENT, 4 - padding(channels))
-
- // Allocate texture storage
- // TODO: Might want to figure out the data type based on the input
- glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, GL_UNSIGNED_BYTE, 0)
+ if (!texture.initialized) throw IllegalStateException("Cannot use uninitialized textures for pixel buffers")
- // Set the texture parameters
+ // We can't call the texture's bind method because the animated texture updates the
+ // data when binding the texture, causing a null pointer exception due to the animated
+ // texture object not being initialized
+ glBindTexture(GL_TEXTURE_2D, texture.id)
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0, texture.format, GL_UNSIGNED_BYTE, 0)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
- // Unbind the texture
- glBindTexture(GL_TEXTURE_2D, 0)
-
- // Fill the storage with null
storage(size)
}
@@ -139,15 +110,5 @@ class PixelBuffer(
GL_RGBA to 4,
GL_BGRA to 4,
)
-
- /**
- * Returns an internal format based on how many channels there are
- */
- private val reverseChannelMapping = mapOf(
- 1 to GL_RED,
- 2 to GL_RG,
- 3 to GL_RGB,
- 4 to GL_RGBA,
- )
}
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt
index 20cc67c34..116c16a85 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/ElementBuffer.kt
@@ -17,19 +17,18 @@
package com.lambda.graphics.buffer.vertex
-import com.lambda.graphics.buffer.IBuffer
+import com.lambda.graphics.buffer.Buffer
import com.lambda.graphics.buffer.vertex.attributes.VertexMode
import com.lambda.graphics.gl.kibibyte
import org.lwjgl.opengl.GL30C.*
import java.nio.ByteBuffer
-class ElementBuffer(mode: VertexMode) : IBuffer {
- override val buffers: Int = 1
- override val usage: Int = GL_DYNAMIC_DRAW
+class ElementBuffer(
+ mode: VertexMode,
+ override val usage: Int = GL_DYNAMIC_DRAW,
+) : Buffer(buffers = 1) {
override val target: Int = GL_ELEMENT_ARRAY_BUFFER
override val access: Int = GL_MAP_WRITE_BIT
- override var index = 0
- override val bufferIds = intArrayOf(glGenBuffers())
override fun upload(
data: ByteBuffer,
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt
index 1043f98c9..5362c6ad4 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexArray.kt
@@ -17,29 +17,26 @@
package com.lambda.graphics.buffer.vertex
-import com.lambda.graphics.buffer.IBuffer
+import com.lambda.graphics.buffer.Buffer
import net.minecraft.client.render.BufferRenderer
import org.lwjgl.opengl.GL30C.*
import java.nio.ByteBuffer
-class VertexArray : IBuffer {
- override val buffers: Int = 1
+class VertexArray : Buffer(isVertexArray = true) {
override val usage: Int = -1
override val target: Int = -1
override val access: Int = -1
- override var index = 0
- override val bufferIds = intArrayOf(glGenVertexArrays())
override fun map(
- offset: Long,
size: Long,
+ offset: Long,
block: (ByteBuffer) -> Unit
- ): Throwable? = throw UnsupportedOperationException("Cannot map a vertex array object to memory")
+ ): Throwable = throw UnsupportedOperationException("Cannot map a vertex array object to memory")
override fun upload(
data: ByteBuffer,
offset: Long,
- ): Throwable? = throw UnsupportedOperationException("Data cannot be uploaded to a vertex array object")
+ ): Throwable = throw UnsupportedOperationException("Data cannot be uploaded to a vertex array object")
override fun allocate(size: Long) = throw UnsupportedOperationException("Cannot grow a vertex array object")
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt
index 91977535d..94358fb09 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/VertexBuffer.kt
@@ -17,24 +17,20 @@
package com.lambda.graphics.buffer.vertex
-import com.lambda.graphics.buffer.IBuffer
+import com.lambda.graphics.buffer.Buffer
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
import com.lambda.graphics.buffer.vertex.attributes.VertexMode
import com.lambda.graphics.gl.kibibyte
-import com.lambda.graphics.gl.putTo
import org.lwjgl.opengl.GL30C.*
import java.nio.ByteBuffer
class VertexBuffer(
mode: VertexMode,
attributes: VertexAttrib.Group,
-) : IBuffer {
- override val buffers: Int = 1
override val usage: Int = GL_DYNAMIC_DRAW
+) : Buffer(buffers = 1) {
override val target: Int = GL_ARRAY_BUFFER
override val access: Int = GL_MAP_WRITE_BIT
- override var index = 0
- override val bufferIds = intArrayOf(glGenBuffers())
override fun upload(
data: ByteBuffer,
diff --git a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt
index 729334ac7..3ece85693 100644
--- a/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/buffer/vertex/attributes/VertexAttrib.kt
@@ -38,15 +38,41 @@ enum class VertexAttrib(
POS_UV(Vec2, Vec2),
// GUI
- FONT(Vec3, Vec2, Color), // pos, uv, color
- RECT_FILLED(Vec2, Vec2, Vec2, Float, Float, Color), // pos, uv, size, roundRadius, shade, color
- RECT_OUTLINE(Vec2, Float, Float, Color), // pos, alpha, shade, color
+ FONT(
+ Vec3, // pos
+ Vec2, // uv
+ Color
+ ),
+
+ RECT_FILLED(
+ Vec3, // pos
+ Vec2, // uv
+ Color
+ ),
+
+ RECT_OUTLINE(
+ Vec3, // pos
+ Vec2, // uv
+ Float, // alpha
+ Color
+ ),
// WORLD
- DYNAMIC_RENDERER(Vec3, Vec3, Color), // prev pos, pos, color
- STATIC_RENDERER(Vec3, Color), // pos, color
+ DYNAMIC_RENDERER(
+ Vec3, // prev pos
+ Vec3, // pos
+ Color
+ ),
+
+ STATIC_RENDERER(
+ Vec3, // pos
+ Color
+ ),
- PARTICLE(Vec3, Vec2, Color); // pos, uv, color
+ PARTICLE(Vec3,
+ Vec2, // pos
+ Color
+ );
val stride = attributes.sumOf { attribute -> attribute.size }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt
index 1b9f15857..ddcefd46e 100644
--- a/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/gl/GlStateUtils.kt
@@ -46,9 +46,11 @@ object GlStateUtils {
cull(savedCull)
}
- fun withDepth(block: () -> Unit) {
+ fun withDepth(maskWrite: Boolean = false, block: () -> Unit) {
depthTest(true)
+ if (maskWrite) glDepthMask(true)
block()
+ if (maskWrite) glDepthMask(false)
depthTest(false)
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt
index 5d8c9b058..b517c4127 100644
--- a/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/gl/Matrices.kt
@@ -22,64 +22,156 @@ import net.minecraft.util.math.RotationAxis
import net.minecraft.util.math.Vec3d
import org.joml.*
+/**
+ * A utility object for managing OpenGL transformation matrices.
+ * Provides a stack-based approach to matrix operations such as translation, scaling,
+ * and world projection building, with optional support for vertex transformations.
+ */
object Matrices {
- private val stack = ArrayDeque(1)
-
+ /**
+ * A stack of 4x4 transformation matrices.
+ */
+ private val stack = ArrayDeque(listOf(Matrix4f()))
+
+ /**
+ * An optional matrix for applying vertex transformations.
+ */
var vertexTransformer: Matrix4d? = null
- fun translate(x: Double, y: Double, z: Double) {
- translate(x.toFloat(), y.toFloat(), z.toFloat())
- }
-
- fun translate(x: Float, y: Float, z: Float) {
- stack.last().translate(x, y, z)
- }
-
- fun scale(x: Double, y: Double, z: Double) {
- stack.last().scale(x.toFloat(), y.toFloat(), z.toFloat())
+ /**
+ * An optional vec3 offset for applying vertex transformations.
+ */
+ var vertexOffset: Vec3d? = null
+
+ /**
+ * Executes a block of code within the context of a new matrix.
+ * The current matrix is pushed onto the stack before the block executes and popped after the block completes.
+ *
+ * Push and pop operations are essential for managing hierarchical transformations in OpenGL.
+ * - `push`: Saves the current matrix state to allow local transformations.
+ * - `block`: Code that uses the modified matrix (ex: rendering)
+ * - `pop`: Restores the previous state and effectively reverts any changes.
+ *
+ * @param block The block of code to execute within the context of the new matrix.
+ */
+ fun push(block: Matrices.() -> Unit) {
+ push()
+ block.invoke(this)
+ pop()
}
- fun scale(x: Float, y: Float, z: Float) {
- stack.last().scale(x, y, z)
+ /**
+ * Pushes a copy of the current matrix onto the stack.
+ */
+ fun push() {
+ val entry = stack.last()
+ stack.addLast(Matrix4f(entry))
}
- fun multiply(quaternion: Quaternionf) {
- stack.last().rotate(quaternion)
+ /**
+ * Removes the top matrix from the stack.
+ *
+ * @throws NoSuchElementException If the stack is empty.
+ */
+ fun pop() {
+ stack.removeLast()
}
- fun multiply(quaternion: Quaternionf, originX: Float, originY: Float, originZ: Float) {
- stack.last().rotateAround(quaternion, originX, originY, originZ)
+ /**
+ * Applies a translation to the top matrix on the transformation stack
+ *
+ * @param x The translation amount along the X axis.
+ * @param y The translation amount along the Y axis.
+ * @param z The translation amount along the Z axis. Defaults to `0.0`.
+ */
+ fun translate(x: Double, y: Double, z: Double = 0.0) {
+ translate(x.toFloat(), y.toFloat(), z.toFloat())
}
- fun push() {
- val entry = stack.last()
- stack.addLast(Matrix4f(entry))
+ /**
+ * Applies a translation to the top matrix on the transformation stack
+ *
+ * @param x The translation amount along the X axis.
+ * @param y The translation amount along the Y axis.
+ * @param z The translation amount along the Z axis. Defaults to `0f`.
+ */
+ fun translate(x: Float, y: Float, z: Float = 0f) {
+ stack.last().translate(x, y, z)
}
- fun push(block: Matrices.() -> Unit) {
- push()
- this.block()
- pop()
+ /**
+ * Scales the current matrix by the given x, y, and z factors.
+ *
+ * @param x The scaling factor along the X axis.
+ * @param y The scaling factor along the Y axis.
+ * @param z The scaling factor along the Z axis. Defaults to `1.0`.
+ */
+ fun scale(x: Double, y: Double, z: Double = 1.0) {
+ stack.last().scale(x.toFloat(), y.toFloat(), z.toFloat())
}
- fun pop() {
- stack.removeLast()
+ /**
+ * Scales the current matrix by the given x, y, and z factors.
+ *
+ * @param x The scaling factor along the X axis.
+ * @param y The scaling factor along the Y axis.
+ * @param z The scaling factor along the Z axis. Defaults to `1f`.
+ */
+ fun scale(x: Float, y: Float, z: Float = 1f) {
+ stack.last().scale(x, y, z)
}
- fun peek() = stack.last()
-
+ /**
+ * Retrieves the current matrix from the stack without removing it.
+ *
+ * @throws NoSuchElementException if the stack is empty
+ * @return The top matrix on the stack
+ */
+ fun peek(): Matrix4f = stack.last()
+
+ /**
+ * Resets the matrix stack with a single initial matrix.
+ *
+ * @param entry The matrix to initialize the stack with.
+ */
fun resetMatrices(entry: Matrix4f) {
stack.clear()
stack.add(entry)
}
+ /**
+ * Temporarily sets a vertex transformation matrix for the duration of a block.
+ *
+ * @param matrix The transformation matrix to apply to vertices.
+ * @param block The block of code to execute with the transformation applied.
+ */
fun withVertexTransform(matrix: Matrix4f, block: () -> Unit) {
vertexTransformer = Matrix4d(matrix)
block()
vertexTransformer = null
}
- fun buildWorldProjection(pos: Vec3d, scale: Double = 1.0, mode: ProjRotationMode = ProjRotationMode.TO_CAMERA) =
+ /**
+ * Applies a temporary vertex offset to mitigate precision issues in matrix operations on large coordinates
+ *
+ * @param offset the offset to apply to vertices for reducing precision loss
+ * @param block the block of code within which the vertex offset is active
+ */
+ fun withVertexOffset(offset: Vec3d, block: () -> Unit) {
+ vertexOffset = offset
+ block()
+ vertexOffset = null
+ }
+
+ /**
+ * Builds a world projection matrix for a given position, scale, and rotation mode.
+ *
+ * @param pos The position in world coordinates.
+ * @param scale The scaling factor. Defaults to `1.0`.
+ * @param mode The rotation mode to apply. Defaults to [ProjRotationMode.TO_CAMERA].
+ * @return A [Matrix4f] representing the world projection.
+ */
+ fun buildWorldProjection(pos: Vec3d, scale: Double = 1.0, mode: ProjRotationMode = ProjRotationMode.TO_CAMERA): Matrix4f =
Matrix4f().apply {
val s = 0.025f * scale.toFloat()
@@ -93,6 +185,9 @@ object Matrices {
scale(-s, -s, s)
}
+ /**
+ * Modes for determining the rotation of the world projection.
+ */
enum class ProjRotationMode {
TO_CAMERA,
UP
diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt
index 8b1ec9708..c99fe6d68 100644
--- a/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/gl/Memory.kt
@@ -113,20 +113,4 @@ val Long.kibibyte get() = this * 1024
val Long.mebibyte get() = this * 1024 * 1024
val Long.gibibyte get() = this * 1024 * 1024 * 1024
-/**
- * Returns memory alignment for each CPU architecture
- */
-fun alignment(): Int {
- return when (System.getProperty("os.arch")?.lowercase()) {
- "x86", "x86_64" -> 4 // 32-bit or 64-bit x86
- "arm", "armv7l", "aarch64" -> 4 // ARM architectures
- else -> 8 // Default to 8 bytes alignment for other architectures
- }
-}
-
-/**
- * Returns how many bytes will be added to reach memory alignment
- */
-fun padding(size: Int): Int = size % alignment() / 8
-
fun ByteBuffer.putTo(dst: ByteBuffer) { dst.put(this) }
diff --git a/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt
similarity index 60%
rename from common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt
rename to common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt
index 24a7be49f..dc87664b9 100644
--- a/common/src/main/kotlin/com/lambda/graphics/gl/Scissor.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/pipeline/ScissorAdapter.kt
@@ -15,54 +15,45 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.gl
+package com.lambda.graphics.pipeline
import com.lambda.Lambda.mc
-import com.lambda.module.modules.client.GuiSettings
+import com.lambda.graphics.RenderMain
import com.lambda.util.math.MathUtils.ceilToInt
import com.lambda.util.math.MathUtils.floorToInt
import com.lambda.util.math.Rect
-import com.mojang.blaze3d.systems.RenderSystem
-import kotlin.math.max
+import com.mojang.blaze3d.systems.RenderSystem.disableScissor
+import com.mojang.blaze3d.systems.RenderSystem.enableScissor
-object Scissor {
+object ScissorAdapter {
private var stack = ArrayDeque()
fun scissor(rect: Rect, block: () -> Unit) {
- // clamp corners so children scissor boxes can't overlap parent
val processed = stack.lastOrNull()?.let(rect::clamp) ?: rect
- registerScissor(processed, block)
- }
- private fun registerScissor(rect: Rect, block: () -> Unit) {
- stack.add(rect)
+ stack.add(processed)
+ scissorRect(processed)
- scissor(rect)
block()
stack.removeLast()
- scissor(stack.lastOrNull())
+ stack.lastOrNull()?.let { scissorRect(it) } ?: disableScissor()
}
- private fun scissor(entry: Rect?) {
- if (entry == null) {
- RenderSystem.disableScissor()
- return
- }
-
- val pos1 = entry.leftTop * GuiSettings.scale
- val pos2 = entry.rightBottom * GuiSettings.scale
+ private fun scissorRect(rect: Rect) {
+ val pos1 = rect.leftTop * RenderMain.scaleFactor
+ val pos2 = rect.rightBottom * RenderMain.scaleFactor
- val width = max(pos2.x - pos1.x, 1.0)
- val height = max(pos2.y - pos1.y, 1.0)
+ val width = (pos2.x - pos1.x).coerceAtLeast(0.0)
+ val height = (pos2.y - pos1.y).coerceAtLeast(0.0)
val y = mc.window.framebufferHeight - pos1.y - height
- RenderSystem.enableScissor(
+ enableScissor(
pos1.x.floorToInt(),
y.floorToInt(),
width.ceilToInt(),
height.ceilToInt()
)
}
-}
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt
index b17471abc..98ce0937a 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ChunkedESP.kt
@@ -22,7 +22,6 @@ import com.lambda.event.events.TickEvent
import com.lambda.event.events.WorldEvent
import com.lambda.event.listener.SafeListener.Companion.listenConcurrently
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.renderer.esp.impl.ESPRenderer
import com.lambda.graphics.renderer.esp.impl.StaticESPRenderer
import com.lambda.module.modules.client.RenderSettings
import com.lambda.threading.awaitMainThread
@@ -100,7 +99,7 @@ class ChunkedESP private constructor(
}
private class EspChunk(val chunk: WorldChunk, val owner: ChunkedESP) {
- var renderer: ESPRenderer? = null
+ var renderer: StaticESPRenderer? = null
private val chunkOffsets = listOf(1 to 0, 0 to 1, -1 to 0, 0 to -1)
@@ -119,7 +118,7 @@ class ChunkedESP private constructor(
}
suspend fun rebuild() {
- val newRenderer = awaitMainThread { StaticESPRenderer() }
+ val newRenderer = awaitMainThread { StaticESPRenderer(false) }
iterateChunk { x, y, z ->
owner.update(newRenderer, chunk.world, x, y, z)
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt
similarity index 76%
rename from common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt
rename to common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt
index f8eba5060..3087e16c0 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/ESPRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/ESPRenderer.kt
@@ -15,19 +15,19 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.renderer.esp.impl
+package com.lambda.graphics.renderer.esp
-import com.lambda.Lambda.mc
+import com.lambda.Lambda
import com.lambda.graphics.buffer.VertexPipeline
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.gl.GlStateUtils.withFaceCulling
-import com.lambda.graphics.gl.GlStateUtils.withLineWidth
+import com.lambda.graphics.gl.GlStateUtils
import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.shader.Shader.Companion.shader
import com.lambda.module.modules.client.RenderSettings
import com.lambda.util.extension.partialTicks
-abstract class ESPRenderer(tickedMode: Boolean) {
+open class ESPRenderer(tickedMode: Boolean) {
val shader: Shader
val faces: VertexPipeline
val outlines: VertexPipeline
@@ -47,11 +47,11 @@ abstract class ESPRenderer(tickedMode: Boolean) {
fun render() {
shader.use()
- shader["u_TickDelta"] = mc.partialTicks
- shader["u_CameraPosition"] = mc.gameRenderer.camera.pos
+ shader["u_TickDelta"] = Lambda.mc.partialTicks
+ shader["u_CameraPosition"] = Lambda.mc.gameRenderer.camera.pos
- withFaceCulling(faces::render)
- withLineWidth(RenderSettings.outlineWidth, outlines::render)
+ GlStateUtils.withFaceCulling(faces::render)
+ GlStateUtils.withLineWidth(RenderSettings.outlineWidth, outlines::render)
}
open fun clear() {
@@ -60,14 +60,14 @@ abstract class ESPRenderer(tickedMode: Boolean) {
}
companion object {
- private val staticMode = Shader(
+ private val staticMode = shader(
"renderer/pos_color",
"renderer/box_static"
) to VertexAttrib.Group.STATIC_RENDERER
- private val dynamicMode = Shader(
+ private val dynamicMode = shader(
"renderer/pos_color",
"renderer/box_dynamic"
) to VertexAttrib.Group.DYNAMIC_RENDERER
}
-}
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt
index 32ac305fa..ceea2084f 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/global/DynamicESP.kt
@@ -27,7 +27,7 @@ object DynamicESP : DynamicESPRenderer() {
init {
listen {
clear()
- RenderEvent.DynamicESP().post()
+ RenderEvent.StaticESP().post()
upload()
}
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt
index b3590251a..34edd1ed5 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/DynamicESPRenderer.kt
@@ -17,4 +17,6 @@
package com.lambda.graphics.renderer.esp.impl
+import com.lambda.graphics.renderer.esp.ESPRenderer
+
open class DynamicESPRenderer : ESPRenderer(true)
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt
index 922883bbd..10fa1349b 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/esp/impl/StaticESPRenderer.kt
@@ -18,6 +18,7 @@
package com.lambda.graphics.renderer.esp.impl
import com.lambda.graphics.buffer.IRenderContext
+import com.lambda.graphics.renderer.esp.ESPRenderer
import java.awt.Color
import java.util.concurrent.ConcurrentHashMap
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt
similarity index 57%
rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt
rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt
index cc484dc94..3bd9a33c0 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/AbstractRectRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/AbstractGUIRenderer.kt
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 Lambda
+ * Copyright 2025 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,33 +15,45 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.renderer.gui.rect
+package com.lambda.graphics.renderer.gui
import com.lambda.graphics.RenderMain
import com.lambda.graphics.buffer.VertexPipeline
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
import com.lambda.graphics.buffer.vertex.attributes.VertexMode
+import com.lambda.graphics.pipeline.ScissorAdapter
import com.lambda.graphics.shader.Shader
import com.lambda.module.modules.client.GuiSettings
+import com.lambda.util.math.MathUtils.toInt
+import com.lambda.util.math.Rect
import com.lambda.util.math.Vec2d
-import org.lwjgl.glfw.GLFW.glfwGetTime
+import org.lwjgl.glfw.GLFW
-abstract class AbstractRectRenderer(
+abstract class AbstractGUIRenderer(
attribGroup: VertexAttrib.Group,
val shader: Shader
) {
- protected val pipeline = VertexPipeline(VertexMode.TRIANGLES, attribGroup)
+ private val pipeline = VertexPipeline(VertexMode.TRIANGLES, attribGroup)
- fun render() {
+ protected fun render(
+ shade: Boolean = false,
+ block: VertexPipeline.() -> Unit
+ ) {
+ pipeline.clear()
shader.use()
- shader["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0
- shader["u_Color1"] = GuiSettings.shadeColor1
- shader["u_Color2"] = GuiSettings.shadeColor2
- shader["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight)
+ block(pipeline)
+
+ shader["u_Shade"] = shade.toInt().toDouble()
+ if (shade) {
+ shader["u_ShadeTime"] = GLFW.glfwGetTime() * GuiSettings.colorSpeed * 5.0
+ shader["u_ShadeColor1"] = GuiSettings.shadeColor1
+ shader["u_ShadeColor2"] = GuiSettings.shadeColor2
+
+ shader["u_ShadeSize"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight)
+ }
pipeline.upload()
pipeline.render()
- pipeline.clear()
}
-}
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt
index 82186f8d1..b8a736f64 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/TextureRenderer.kt
@@ -21,7 +21,7 @@ import com.lambda.graphics.RenderMain
import com.lambda.graphics.buffer.VertexPipeline
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.shader.Shader.Companion.shader
import com.lambda.graphics.texture.Texture
import com.lambda.module.modules.client.GuiSettings
import com.lambda.util.math.Rect
@@ -30,24 +30,25 @@ import org.lwjgl.glfw.GLFW.glfwGetTime
object TextureRenderer {
private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.POS_UV)
- private val shader = Shader("renderer/pos_tex")
- private val shaderColored = Shader("renderer/pos_tex_shady")
+
+ private val mainShader = shader("renderer/pos_tex")
+ private val coloredShader = shader("renderer/pos_tex_shady")
fun drawTexture(texture: Texture, rect: Rect) {
texture.bind()
- shader.use()
+ mainShader.use()
drawInternal(rect)
}
fun drawTextureShaded(texture: Texture, rect: Rect) {
texture.bind()
- shaderColored.use()
+ coloredShader.use()
- shaderColored["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0
- shaderColored["u_Color1"] = GuiSettings.shadeColor1
- shaderColored["u_Color2"] = GuiSettings.shadeColor2
- shaderColored["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight)
+ coloredShader["u_Time"] = glfwGetTime() * GuiSettings.colorSpeed * 5.0
+ coloredShader["u_Color1"] = GuiSettings.shadeColor1
+ coloredShader["u_Color2"] = GuiSettings.shadeColor2
+ coloredShader["u_Size"] = RenderMain.screenSize / Vec2d(GuiSettings.colorWidth, GuiSettings.colorHeight)
drawInternal(rect)
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt
index 68072de6c..16fe5d96a 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/FontRenderer.kt
@@ -19,177 +19,261 @@ package com.lambda.graphics.renderer.gui.font
import com.lambda.graphics.buffer.VertexPipeline
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.buffer.vertex.attributes.VertexMode
-import com.lambda.graphics.renderer.gui.font.glyph.GlyphInfo
-import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.renderer.gui.AbstractGUIRenderer
+import com.lambda.graphics.renderer.gui.font.core.GlyphInfo
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.height
+import com.lambda.graphics.shader.Shader.Companion.shader
+import com.lambda.graphics.texture.TextureOwner.bind
import com.lambda.module.modules.client.LambdaMoji
import com.lambda.module.modules.client.RenderSettings
+import com.lambda.util.math.MathUtils.toInt
import com.lambda.util.math.Vec2d
import com.lambda.util.math.a
import com.lambda.util.math.setAlpha
import java.awt.Color
-class FontRenderer(
- private val font: LambdaFont,
- private val emojis: LambdaEmoji
-) {
- private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.FONT)
+/**
+ * Renders text and emoji glyphs using a shader-based font rendering system.
+ * This class handles text and emoji rendering, shadow effects, and text scaling.
+ */
+object FontRenderer : AbstractGUIRenderer(VertexAttrib.Group.FONT, shader("font/font")) {
+ private val chars get() = RenderSettings.textFont
+ private val emojis get() = RenderSettings.emojiFont
- var scaleMultiplier = 1.0
+ private val shadowShift get() = RenderSettings.shadowShift * 10.0
+ private val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 16f
+ private val gap get() = RenderSettings.gap * 0.5f - 0.8f
/**
- * Builds the vertex array for rendering the text.
+ * Renders a text string at a specified position with configurable color, scale, shadow, and emoji parsing
+ *
+ * @param text The text to render.
+ * @param position The position to render the text.
+ * @param color The color of the text.
+ * @param scale The scale factor of the text.
+ * @param shadow Whether to render a shadow for the text.
+ * @param parseEmoji Whether to parse and render emojis in the text.
*/
- fun build(
+ fun drawString(
text: String,
- position: Vec2d,
+ position: Vec2d = Vec2d.ZERO,
color: Color = Color.WHITE,
scale: Double = 1.0,
shadow: Boolean = true,
- ) = pipeline.use {
- iterateText(text, scale, shadow, color) { char, pos1, pos2, color ->
- grow(4)
- putQuad(
- vec3m(pos1.x + position.x, pos1.y + position.y, 0.0).vec2(char.uv1.x, char.uv1.y).color(color).end(),
- vec3m(pos1.x + position.x, pos2.y + position.y, 0.0).vec2(char.uv1.x, char.uv2.y).color(color).end(),
- vec3m(pos2.x + position.x, pos2.y + position.y, 0.0).vec2(char.uv2.x, char.uv2.y).color(color).end(),
- vec3m(pos2.x + position.x, pos1.y + position.y, 0.0).vec2(char.uv2.x, char.uv1.y).color(color).end()
- )
+ parseEmoji: Boolean = LambdaMoji.isEnabled
+ ) = render {
+ shader["u_FontTexture"] = 0
+ shader["u_EmojiTexture"] = 1
+ shader["u_SDFMin"] = RenderSettings.sdfMin
+ shader["u_SDFMax"] = RenderSettings.sdfMax
+
+ bind(chars, emojis)
+
+ processText(text, color, scale, shadow, parseEmoji) { char, pos1, pos2, col, _ ->
+ buildGlyph(char, position, pos1, pos2, col)
}
}
/**
- * Calculates the width of the given text.
+ * Renders a single glyph at the specified position with the given scale and color
+ *
+ * @param glyph The glyph information
+ * @param position The rendering position where the glyph will be drawn
+ * @param color The color of the glyph
+ * @param scale The scale factor of the glyph
+ */
+ fun drawGlyph(
+ glyph: GlyphInfo,
+ position: Vec2d,
+ color: Color = Color.WHITE,
+ scale: Double = 1.0
+ ) = render {
+ shader["u_FontTexture"] = 0
+ shader["u_EmojiTexture"] = 1
+ shader["u_SDFMin"] = RenderSettings.sdfMin
+ shader["u_SDFMax"] = RenderSettings.sdfMax
+
+ bind(chars, emojis)
+
+ val actualScale = getScaleFactor(scale)
+ val scaledSize = glyph.size * actualScale
+
+ val posY = getHeight(scale) * -0.5 + baselineOffset * actualScale
+ val pos1 = Vec2d(0.0, posY) * actualScale
+ val pos2 = pos1 + scaledSize
+
+ buildGlyph(glyph, position, pos1, pos2, color)
+ }
+
+ /**
+ * Renders a single glyph at a given position.
+ *
+ * @param glyph The glyph information to render.
+ * @param origin The position to start from
+ * @param pos1 The starting position of the glyph.
+ * @param pos2 The end position of the glyph
+ * @param color The color of the glyph.
+ */
+ private fun VertexPipeline.buildGlyph(
+ glyph: GlyphInfo,
+ origin: Vec2d = Vec2d.ZERO,
+ pos1: Vec2d,
+ pos2: Vec2d,
+ color: Color,
+ ) {
+ val x1 = pos1.x + origin.x
+ val y1 = pos1.y + origin.y
+ val x2 = pos2.x + origin.x
+ val y2 = pos2.y + origin.y
+
+ grow(4)
+
+ putQuad(
+ vec3m(x1, y1, 0.0).vec2(glyph.uv1.x, glyph.uv1.y).color(color).end(),
+ vec3m(x1, y2, 0.0).vec2(glyph.uv1.x, glyph.uv2.y).color(color).end(),
+ vec3m(x2, y2, 0.0).vec2(glyph.uv2.x, glyph.uv2.y).color(color).end(),
+ vec3m(x2, y1, 0.0).vec2(glyph.uv2.x, glyph.uv1.y).color(color).end()
+ )
+ }
+
+ /**
+ * Calculates the width of the specified text.
+ *
+ * @param text The text to measure.
+ * @param scale The scale factor for the width calculation.
+ * @param parseEmoji Whether to include emojis in the width calculation.
+ * @return The width of the text at the specified scale.
*/
- fun getWidth(text: String, scale: Double = 1.0): Double {
+ fun getWidth(
+ text: String,
+ scale: Double = 1.0,
+ parseEmoji: Boolean = LambdaMoji.isEnabled,
+ ): Double {
var width = 0.0
- iterateText(text, scale, false) { char, _, _, _ -> width += char.width + gap }
- return width * getScaleFactor(scale)
+ var gaps = -1
+
+ processText(text, scale = scale, parseEmoji = parseEmoji) { char, _, _, _, isShadow ->
+ if (isShadow) return@processText
+ width += char.width; gaps++
+ }
+
+ return (width + gaps.coerceAtLeast(0) * gap) * getScaleFactor(scale)
}
/**
- * Calculates the height of the text.
+ * Computes the effective height of the rendered text
+ *
+ * The height is derived from the current font's base height, adjusted by a scaling factor
+ * that ensures consistent visual proportions
*
- * The values are hardcoded
- * We do not need to ask the emoji font since the height is smaller
+ * @param scale The scale factor for the height calculation.
+ * @return The height of the text at the specified scale.
*/
- fun getHeight(scale: Double = 1.0) = font.glyphs.fontHeight * getScaleFactor(scale) * 0.7
+ fun getHeight(scale: Double = 1.0) = chars.height * getScaleFactor(scale) * 0.7
/**
- * Iterates over each character and emoji in the text.
+ * Processes a text string by iterating over its characters and emojis, computing rendering positions, and invoking a block for each glyph
*
* @param text The text to iterate over.
+ * @param color The color of the text.
* @param scale The scale of the text.
* @param shadow Whether to render a shadow.
- * @param color The color of the text.
- * @param block The block to execute for each character.
- *
- * @see GlyphInfo
+ * @param parseEmoji Whether to parse and include emojis.
+ * @param block The function to apply to each character or emoji glyph.
*/
- private fun iterateText(
+ private fun processText(
text: String,
- scale: Double,
- shadow: Boolean,
color: Color = Color.WHITE,
- block: (GlyphInfo, Vec2d, Vec2d, Color) -> Unit
+ scale: Double = 1.0,
+ shadow: Boolean = RenderSettings.shadow,
+ parseEmoji: Boolean = LambdaMoji.isEnabled,
+ block: (GlyphInfo, Vec2d, Vec2d, Color, Boolean) -> Unit
) {
val actualScale = getScaleFactor(scale)
val scaledGap = gap * actualScale
val shadowColor = getShadowColor(color)
- val emojiColor = Color.WHITE.setAlpha(color.a)
+ val emojiColor = color.setAlpha(color.a)
var posX = 0.0
- val posY = getHeight(scale) * -0.5 + baselineOffset * actualScale
+ var posY = getHeight(scale) * -0.5 + baselineOffset * actualScale
- val emojis = parseEmojis(text, emojis)
+ fun drawGlyph(info: GlyphInfo?, color: Color, isShadow: Boolean = false) {
+ if (info == null) return
- fun draw(info: GlyphInfo, color: Color, offset: Double = 0.0) {
val scaledSize = info.size * actualScale
- val pos1 = Vec2d(posX, posY) + offset * actualScale
+ val pos1 = Vec2d(posX, posY) + shadowShift * actualScale * isShadow.toInt()
val pos2 = pos1 + scaledSize
- block(info, pos1, pos2, color)
- if (offset == 0.0) posX += scaledSize.x + scaledGap
+ block(info, pos1, pos2, color, isShadow)
+ if (!isShadow) posX += scaledSize.x + scaledGap
}
- var index = 0
- textProcessor@ while (index < text.length) {
- var innerLoopContact = false // Instead of using BreakContinueInInlineLambdas, we use this
+ val parsed = if (parseEmoji) emojis.parse(text) else mutableListOf()
+
+ fun processTextSection(section: String, hasEmojis: Boolean) {
+ if (section.isEmpty()) return
+ if (!parseEmoji || parsed.isEmpty() || !hasEmojis) {
+ // Draw simple characters if no emojis are present
+ section.forEach { char ->
+ // Logic for control characters
+ when (char) {
+ '\n', '\r' -> { posX = 0.0; posY += chars.height * actualScale; return@forEach }
+ }
- if (LambdaMoji.isEnabled) {
- // Check if there are emojis to render
- emojis.firstOrNull { index in it.second }?.let { emoji ->
- if (index == emoji.second.first) draw(emoji.first, emojiColor)
+ val glyph = chars[char] ?: return@forEach
- // Skip the emoji
- index = emoji.second.last + 1
- innerLoopContact = true
+ if (shadow && RenderSettings.shadow) drawGlyph(glyph, shadowColor, true)
+ drawGlyph(glyph, color)
}
- }
+ } else {
+ // Only compute the first parsed emoji to avoid duplication
+ // This is important in order to keep the parsed ranges valid
+ // If you do not this, you will get out of bounds positions
+ // due to slicing
+ val emoji = parsed.removeFirstOrNull() ?: return
- if (innerLoopContact) continue@textProcessor
+ // Iterate the emojis from left to right
+ val start = section.indexOf(emoji)
+ val end = start + emoji.length
- // Render chars
- val charInfo = font[text[index]] ?: continue@textProcessor
+ val preEmojiText = section.substring(0, start)
+ val postEmojiText = section.substring(end)
- // Draw a shadow before
- if (shadow && RenderSettings.shadow && shadowShift > 0.0) {
- draw(charInfo, shadowColor, shadowShift)
- }
+ // Draw the text without emoji
+ processTextSection(preEmojiText, hasEmojis = false)
- // Draw actual char over the shadow
- draw(charInfo, color)
+ // Draw the emoji
+ drawGlyph(emojis[emoji], emojiColor)
- index++
+ // Process the rest of the text after the emoji
+ processTextSection(postEmojiText, hasEmojis = true)
+ }
}
- }
- private fun getScaleFactor(scale: Double) = scaleMultiplier * scale * 0.12
-
- private fun getShadowColor(color: Color): Color {
- return Color(
- (color.red * RenderSettings.shadowBrightness).toInt(),
- (color.green * RenderSettings.shadowBrightness).toInt(),
- (color.blue * RenderSettings.shadowBrightness).toInt(),
- color.alpha
- )
+ // Start processing the full text
+ processTextSection(text, hasEmojis = parsed.isNotEmpty())
}
- fun render() {
- shader.use()
- shader["u_EmojiTexture"] = 1
-
- font.glyphs.bind()
- emojis.glyphs.bind()
-
- pipeline.upload()
- pipeline.render()
- pipeline.clear()
- }
+ /**
+ * Calculates the scale factor for the text based on the provided scale.
+ *
+ * @param scale The base scale factor.
+ * @return The adjusted scale factor.
+ */
+ fun getScaleFactor(scale: Double): Double = scale * 8.5 / chars.height
- companion object {
- private val shader = Shader("renderer/font")
-
- val shadowShift get() = RenderSettings.shadowShift * 5.0
- val baselineOffset get() = RenderSettings.baselineOffset * 2.0f - 10f
- val gap get() = RenderSettings.gap * 0.5f - 0.8f
-
- private val emojiRegex = Regex(":[a-zA-Z0-9_]+:")
-
- /**
- * Parses the emojis in the given text.
- *
- * @param text The text to parse.
- * @return A list of pairs containing the glyph info and the range of the emoji in the text.
- */
- fun parseEmojis(text: String, emojis: LambdaEmoji) =
- mutableListOf>().apply {
- emojiRegex.findAll(text).forEach { match ->
- val emojiKey = match.value.substring(1, match.value.length - 1)
- val charInfo = emojis[emojiKey] ?: return@forEach
- add(charInfo to match.range)
- }
- }
- }
+ /**
+ * Calculates the shadow color by adjusting the brightness of the input color.
+ *
+ * @param color The original color.
+ * @return The modified shadow color.
+ */
+ fun getShadowColor(color: Color): Color = Color(
+ (color.red * RenderSettings.shadowBrightness).toInt(),
+ (color.green * RenderSettings.shadowBrightness).toInt(),
+ (color.blue * RenderSettings.shadowBrightness).toInt(),
+ color.alpha
+ )
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt
deleted file mode 100644
index 683d8750d..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaEmoji.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.renderer.gui.font
-
-import com.lambda.core.Loadable
-import com.lambda.graphics.renderer.gui.font.glyph.EmojiGlyphs
-
-enum class LambdaEmoji(private val zipUrl: String) {
- Twemoji("https://github.com/Edouard127/emoji-generator/releases/latest/download/emojis.zip");
-
- lateinit var glyphs: EmojiGlyphs
-
- operator fun get(emoji: String) = glyphs.emojiFromString(emoji)
-
- fun loadGlyphs() {
- glyphs = EmojiGlyphs(zipUrl)
- }
-
- object Loader : Loadable {
- override fun load(): String {
- entries.forEach(LambdaEmoji::loadGlyphs)
- return "Loaded ${entries.size} emoji sets with a total of ${entries.sumOf { it.glyphs.count }} emojis"
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt
deleted file mode 100644
index 5b3c2c219..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/LambdaFont.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.renderer.gui.font
-
-import com.lambda.core.Loadable
-import com.lambda.graphics.renderer.gui.font.glyph.FontGlyphs
-import com.lambda.util.LambdaResource
-import java.awt.Font
-
-enum class LambdaFont(private val fontName: String) {
- FiraSansRegular("FiraSans-Regular"),
- FiraSansBold("FiraSans-Bold");
-
- lateinit var glyphs: FontGlyphs
-
- operator fun get(char: Char) = glyphs.getChar(char)
-
- fun loadGlyphs() {
- val resource = LambdaResource("fonts/$fontName.ttf")
- val stream = resource.stream ?: throw IllegalStateException("Failed to locate font $fontName")
- val font = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(64.0f)
- glyphs = FontGlyphs(font)
- }
-
- object FontLoader : Loadable {
- override fun load(): String {
- entries.forEach(LambdaFont::loadGlyphs)
- return "Loaded ${entries.size} fonts"
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt
similarity index 96%
rename from common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt
rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt
index 691b656c3..e1d9f7f5c 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/GlyphInfo.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/GlyphInfo.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.lambda.graphics.renderer.gui.font.glyph
+package com.lambda.graphics.renderer.gui.font.core
import com.lambda.util.math.Vec2d
@@ -60,4 +60,4 @@ data class GlyphInfo(
* The V coordinate of the bottom-right corner of the character texture.
*/
val v2 get() = uv2.y
-}
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt
new file mode 100644
index 000000000..3bd0027f6
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaAtlas.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.renderer.gui.font.core
+
+import com.google.common.math.IntMath
+import com.lambda.core.Loadable
+import com.lambda.graphics.texture.TextureOwner.uploadField
+import com.lambda.threading.runGameScheduled
+import com.lambda.util.math.Vec2d
+import com.lambda.util.stream
+import com.lambda.util.url
+import it.unimi.dsi.fastutil.objects.Object2DoubleArrayMap
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
+import java.awt.*
+import java.awt.image.BufferedImage
+import java.io.File
+import java.util.zip.ZipFile
+import javax.imageio.ImageIO
+import kotlin.math.ceil
+import kotlin.math.log2
+import kotlin.math.max
+import kotlin.math.sqrt
+
+/**
+ * The [LambdaAtlas] manages the creation and binding of texture atlases for fonts, emojis and user defined atlases
+ * It stores glyph information, manages texture uploads, and provides functionality to build texture buffers for fonts and emoji sets
+ *
+ * It caches font information and emoji data for efficient rendering and includes mechanisms for uploading and binding texture atlases
+ *
+ * It's also possible to upload custom atlases and bind them with no hassle
+ * ```kt
+ * enum class ExampleFont {
+ * CoolFont("Cool-Font");
+ * }
+ *
+ * fun loadFont(...) = BufferedImage
+ *
+ * // Function extension from [TexturePipeline]
+ * ExampleFont.CoolFont.upload(loadFont(...)) // The extension keeps a reference to the font owner
+ *
+ * ...
+ *
+ * onRender {
+ * ExampleFont.CoolFont.bind()
+ * }
+ * ```
+ */
+object LambdaAtlas : Loadable {
+ private val fontMap = mutableMapOf>()
+ private val emojiMap = mutableMapOf>()
+
+ private val bufferPool =
+ mutableMapOf() // This array is nuked once the data is dispatched to OpenGL
+
+ private val fontCache = mutableMapOf()
+ private val metricCache = mutableMapOf()
+ private val heightCache = Object2DoubleArrayMap()
+
+ operator fun LambdaFont.get(char: Char): GlyphInfo? = fontMap.getValue(this)[char]
+ operator fun LambdaEmoji.get(string: String): GlyphInfo? = emojiMap.getValue(this)[string.removeSurrounding(":")]
+
+ val LambdaFont.height: Double
+ get() = heightCache.getDouble(fontCache[this@height])
+
+ val LambdaEmoji.keys
+ get() = emojiMap.getValue(this)
+
+ private const val CHAR_SPACE = 8
+
+ /**
+ * Builds the buffer for an emoji set by reading a ZIP file containing emoji images.
+ * The images are arranged into a texture atlas, and their UV coordinates are computed for later rendering.
+ *
+ * @throws IllegalStateException If the texture size is too small to fit the emojis.
+ */
+ fun LambdaEmoji.buildBuffer() {
+ var image: BufferedImage
+ val file = File.createTempFile("emoji", "zip")
+ url.stream.copyTo(file.outputStream())
+ ZipFile(file).use { zip ->
+ val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement()))
+ val length = zip.size().toDouble()
+
+ val textureDimensionLength: (Int) -> Int = { dimLength ->
+ IntMath.pow(2, ceil(log2((dimLength + CHAR_SPACE) * sqrt(length))).toInt())
+ }
+
+ val width = textureDimensionLength(firstImage.width)
+ val height = textureDimensionLength(firstImage.height)
+ val texelSize = Vec2d.ONE / Vec2d(width, height)
+
+ image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
+ val graphics = image.graphics as Graphics2D
+ graphics.color = Color(0, 0, 0, 0)
+
+ var x = CHAR_SPACE
+ var y = CHAR_SPACE
+
+ val constructed = Object2ObjectOpenHashMap()
+ for (entry in zip.entries()) {
+ val name = entry.name.substringAfterLast("/").substringBeforeLast(".")
+ val emoji = ImageIO.read(zip.getInputStream(entry))
+
+ val charWidth = emoji.width + CHAR_SPACE
+ val charHeight = emoji.height + CHAR_SPACE
+
+ if (x + charWidth >= image.width) {
+ check(y + charHeight < image.height) { "Can't load emoji glyphs. Texture size is too small" }
+
+ y += charHeight
+ x = 0
+ }
+
+ graphics.drawImage(emoji, x, y, null)
+
+ val size = Vec2d(emoji.width, emoji.height)
+ val uv1 = Vec2d(x, y) * texelSize
+ val uv2 = Vec2d(x, y).plus(size) * texelSize
+
+ val normalized = 128.0 / size.y
+ constructed[name] = GlyphInfo(size * normalized, -uv1, -uv2)
+
+ x += emoji.width + 2
+ }
+
+ emojiMap[this@buildBuffer] = constructed
+ }
+
+ bufferPool[this@buildBuffer] = image
+ }
+
+ fun LambdaFont.buildBuffer(
+ characters: Int = 2048 // How many characters from that font should be used for the generation
+ ) {
+ val font = fontCache.computeIfAbsent(this) {
+ Font.createFont(Font.TRUETYPE_FONT, "fonts/$fontName.ttf".stream).deriveFont(128.0f)
+ }
+
+ val textureSize = characters * 2
+ val oneTexelSize = 1.0 / textureSize
+
+ val image = BufferedImage(textureSize, textureSize, BufferedImage.TYPE_INT_ARGB)
+
+ val graphics = image.graphics as Graphics2D
+ graphics.background = Color(0, 0, 0, 0)
+
+ var x = CHAR_SPACE
+ var y = CHAR_SPACE
+ var rowHeight = 0
+
+ val constructed = mutableMapOf()
+ (Char.MIN_VALUE..
+ val charImage = getCharImage(font, char) ?: return@forEach
+
+ rowHeight = max(rowHeight, charImage.height + CHAR_SPACE)
+ val charWidth = charImage.width + CHAR_SPACE
+
+ if (x + charWidth >= textureSize) {
+ // Check if possible to step to the next row
+ check(y + rowHeight <= textureSize) { "Can't load font glyphs. Texture size is too small" }
+
+ y += rowHeight
+ x = 0
+ rowHeight = 0
+ }
+
+ graphics.drawImage(charImage, x, y, null)
+
+ val size = Vec2d(charImage.width, charImage.height)
+ val uv1 = Vec2d(x, y) * oneTexelSize
+ val uv2 = Vec2d(x, y).plus(size) * oneTexelSize
+
+ constructed[char] = GlyphInfo(size, uv1, uv2)
+ heightCache[font] = max(heightCache.getDouble(font), size.y) // No compare set unfortunately
+
+ x += charWidth
+ }
+
+ fontMap[this@buildBuffer] = constructed
+ bufferPool[this@buildBuffer] = image
+ }
+
+ // TODO: Change this when we've refactored the loadables
+ override fun load(): String {
+ LambdaFont.entries.forEach(LambdaFont::load)
+ LambdaEmoji.entries.forEach(LambdaEmoji::load)
+
+ val str = "Loaded ${bufferPool.size} fonts" // avoid race condition
+
+ runGameScheduled {
+ bufferPool.forEach { (owner, image) -> owner.uploadField(image) }
+ bufferPool.clear()
+ }
+
+ return str
+ }
+
+ private fun getCharImage(font: Font, codePoint: Char): BufferedImage? {
+ if (!font.canDisplay(codePoint)) return null
+
+ val fontMetrics = metricCache.getOrPut(font) {
+ val image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
+ val graphics2D = image.createGraphics()
+
+ graphics2D.font = font
+ graphics2D.dispose()
+
+ image.graphics.getFontMetrics(font)
+ }
+
+ val charWidth = if (fontMetrics.charWidth(codePoint) > 0) fontMetrics.charWidth(codePoint) else 8
+ val charHeight = if (fontMetrics.height > 0) fontMetrics.height else font.size
+
+ val charImage = BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB)
+ val graphics2D = charImage.createGraphics()
+
+ graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
+ graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT)
+
+ graphics2D.font = font
+ graphics2D.color = Color.WHITE
+ graphics2D.drawString(codePoint.toString(), 0, fontMetrics.ascent)
+ graphics2D.dispose()
+
+ return charImage
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt
new file mode 100644
index 000000000..7eeb9ba33
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaEmoji.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.renderer.gui.font.core
+
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.buildBuffer
+
+enum class LambdaEmoji(val url: String) {
+ Twemoji("fonts/emojis.zip");
+
+ private val emojiRegex = Regex(":[a-zA-Z0-9_]+:")
+
+ /**
+ * Extracts emoji names from the provided text
+ *
+ * The function scans the input text for patterns matching emojis in the `:name:` format and
+ * returns a mutable list of the emoji names
+ *
+ * @param text The text to parse.
+ * @return A list of extract emoji names
+ */
+ fun parse(text: String): MutableList =
+ emojiRegex.findAll(text).map { it.value }.toMutableList()
+
+ fun load(): String {
+ entries.forEach { it.buildBuffer() }
+ return "Loaded ${entries.size} emoji sets"
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt
similarity index 64%
rename from common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt
rename to common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt
index 5dc49b720..f6d3ed01f 100644
--- a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildComponent.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/core/LambdaFont.kt
@@ -15,11 +15,16 @@
* along with this program. If not, see .
*/
-package com.lambda.gui.api.component.core.list
+package com.lambda.graphics.renderer.gui.font.core
-import com.lambda.gui.api.component.InteractiveComponent
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.buildBuffer
-abstract class ChildComponent(open val owner: ChildLayer<*, *>) : InteractiveComponent() {
- open var accessible = false
- override val hovered; get() = super.hovered && accessible
-}
+enum class LambdaFont(val fontName: String) {
+ FiraSansRegular("FiraSans-Regular"),
+ FiraSansBold("FiraSans-Bold");
+
+ fun load(): String {
+ entries.forEach { it.buildBuffer() }
+ return "Loaded ${entries.size} fonts"
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt
deleted file mode 100644
index ab22c5db0..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/EmojiGlyphs.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.renderer.gui.font.glyph
-
-import com.github.kittinunf.fuel.Fuel
-import com.github.kittinunf.fuel.core.FuelManager
-import com.github.kittinunf.fuel.core.Method
-import com.github.kittinunf.fuel.core.await
-import com.github.kittinunf.fuel.core.awaitResponse
-import com.github.kittinunf.fuel.core.awaitUnit
-import com.github.kittinunf.fuel.core.responseUnit
-import com.google.common.math.IntMath.pow
-import com.lambda.Lambda.LOG
-import com.lambda.graphics.texture.MipmapTexture
-import com.lambda.module.modules.client.RenderSettings
-import com.lambda.util.FolderRegister.cache
-import com.lambda.util.math.Vec2d
-import java.awt.Color
-import java.awt.Graphics2D
-import java.awt.image.BufferedImage
-import java.io.File
-import java.util.zip.ZipFile
-import javax.imageio.ImageIO
-import kotlin.math.ceil
-import kotlin.math.log2
-import kotlin.math.sqrt
-
-class EmojiGlyphs(zipUrl: String) {
- private val emojiMap = mutableMapOf()
- private lateinit var fontTexture: MipmapTexture
-
- private lateinit var image: BufferedImage
- private lateinit var graphics: Graphics2D
-
- val count get() = emojiMap.size
-
- init {
- runCatching {
- downloadAndProcessZip(zipUrl)
- }.onFailure {
- LOG.error("Failed to load emojis: ${it.message}", it)
- fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB))
- }
- }
-
- private fun downloadAndProcessZip(zipUrl: String) {
- val file = cache.resolve("emojis.zip").toFile()
-
- Fuel.download(zipUrl, Method.GET)
- .fileDestination { _, _ -> file }
- .responseUnit()
-
- fontTexture = MipmapTexture(processZip(file))
- }
-
- /**
- * Processes the given zip file and loads the emojis into the texture.
- *
- * @param file The zip file containing the emojis.
- * @return The texture containing the emojis.
- */
- private fun processZip(file: File): BufferedImage {
- ZipFile(file).use { zip ->
- val firstImage = ImageIO.read(zip.getInputStream(zip.entries().nextElement()))
- val length = zip.size().toDouble()
-
- val textureDimensionLength: (Int) -> Int = { dimLength ->
- pow(2, ceil(log2((dimLength + STEP) * sqrt(length))).toInt())
- }
-
- val width = textureDimensionLength(firstImage.width)
- val height = textureDimensionLength(firstImage.height)
- val texelSize = Vec2d.ONE / Vec2d(width, height)
-
- image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
- graphics = image.graphics as Graphics2D
- graphics.color = Color(0, 0, 0, 0)
-
- var x = 0
- var y = 0
-
- for (entry in zip.entries()) {
- val name = entry.name.substringAfterLast("/").substringBeforeLast(".")
- val emoji = ImageIO.read(zip.getInputStream(entry))
-
- if (x + emoji.width >= image.width) {
- y += emoji.height + STEP
- x = 0
- }
-
- check(y + emoji.height < image.height) { "Can't load emoji glyphs. Texture size is too small" }
-
- graphics.drawImage(emoji, x, y, null)
-
- val size = Vec2d(emoji.width, emoji.height)
- val uv1 = Vec2d(x, y) * texelSize
- val uv2 = Vec2d(x, y).plus(size) * texelSize
-
- emojiMap[name] = GlyphInfo(size, -uv1, -uv2)
-
- x += emoji.width + STEP
- }
- }
-
- return image
- }
-
- fun bind() {
- with(fontTexture) {
- bind(GL_TEXTURE_SLOT)
- setLOD(RenderSettings.lodBias.toFloat())
- }
- }
-
- fun emojiFromString(emoji: String) = emojiMap[emoji]
-
- companion object {
- private const val STEP = 2
- private const val GL_TEXTURE_SLOT = 1
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt
deleted file mode 100644
index 72471e733..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/glyph/FontGlyphs.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.renderer.gui.font.glyph
-
-import com.lambda.Lambda.LOG
-import com.lambda.graphics.texture.MipmapTexture
-import com.lambda.graphics.texture.TextureUtils.getCharImage
-import com.lambda.module.modules.client.RenderSettings
-import com.lambda.util.math.Vec2d
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
-import java.awt.Color
-import java.awt.Font
-import java.awt.Graphics2D
-import java.awt.image.BufferedImage
-import kotlin.math.max
-
-class FontGlyphs(
- private val font: Font
-) {
- private val charMap = Int2ObjectOpenHashMap()
- private lateinit var fontTexture: MipmapTexture
-
- var fontHeight = 0.0; private set
-
- init {
- runCatching {
- processGlyphs()
- LOG.info("Loaded ${font.fontName} with ${charMap.size} characters")
- }.onFailure {
- LOG.error("Failed to load font glyphs: ${it.message}", it)
- fontTexture = MipmapTexture(BufferedImage(1024, 1024, BufferedImage.TYPE_INT_ARGB))
- }
- }
-
- private fun processGlyphs() {
- val image = BufferedImage(TEXTURE_SIZE, TEXTURE_SIZE, BufferedImage.TYPE_INT_ARGB)
-
- val graphics = image.graphics as Graphics2D
- graphics.background = Color(0, 0, 0, 0)
-
- var x = 0
- var y = 0
- var rowHeight = 0
-
- (Char.MIN_VALUE..
- val charImage = getCharImage(font, char) ?: return@forEach
-
- rowHeight = max(rowHeight, charImage.height + STEP)
-
- if (x + charImage.width >= TEXTURE_SIZE) {
- y += rowHeight
- x = 0
- rowHeight = 0
- }
-
- check(y + charImage.height <= TEXTURE_SIZE) { "Can't load font glyphs. Texture size is too small" }
-
- graphics.drawImage(charImage, x, y, null)
-
- val size = Vec2d(charImage.width, charImage.height)
- val uv1 = Vec2d(x, y) * ONE_TEXEL_SIZE
- val uv2 = Vec2d(x, y).plus(size) * ONE_TEXEL_SIZE
-
- charMap[char.code] = GlyphInfo(size, uv1, uv2)
- fontHeight = max(fontHeight, size.y)
-
- x += charImage.width + STEP
- }
-
- fontTexture = MipmapTexture(image)
- }
-
- fun bind() {
- with(fontTexture) {
- bind(GL_TEXTURE_SLOT)
- setLOD(RenderSettings.lodBias.toFloat())
- }
- }
-
- fun getChar(char: Char): GlyphInfo? =
- charMap[char.code]
-
- companion object {
- // The allocated texture slot
- private const val GL_TEXTURE_SLOT = 0
-
- // The space between glyphs is necessary to prevent artifacts from appearing when the font texture is blurred
- private const val STEP = 2
-
- // Since most Lambda users probably have bad pc, the default size is 2048, which includes latin, cyrillic, greek and arabic
- // and in the future we could grow the textures when needed
- private const val CHAR_AMOUNT = 2048
-
- // The size of the texture in pixels
- private const val TEXTURE_SIZE = CHAR_AMOUNT * 2
-
- // The size of one texel in UV coordinates
- private const val ONE_TEXEL_SIZE = 1.0 / TEXTURE_SIZE
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt
new file mode 100644
index 000000000..1f8eaf5e0
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/font/sdf/DistanceFieldTexture.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.renderer.gui.font.sdf
+
+import com.lambda.graphics.buffer.frame.CachedFrame
+import com.lambda.graphics.buffer.frame.FrameBuffer
+import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.shader.Shader.Companion.shader
+import com.lambda.graphics.texture.Texture
+import com.lambda.util.math.Vec2d
+import java.awt.image.BufferedImage
+
+/**
+ * A class that represents a distance field texture, which is created by rendering a given texture
+ * onto a framebuffer with specific shader operations.
+ *
+ * The texture is used to create a signed distance field (SDF) for rendering operations.
+ *
+ * @param image Image data to upload
+ */
+class DistanceFieldTexture(image: BufferedImage) : Texture(image, levels = 0) {
+ private val shader = shader("signed_distance_field", "renderer/pos_tex")
+
+ private val frame = CachedFrame(width, height).write {
+ FrameBuffer.pipeline.use {
+ val (pos1, pos2) = Vec2d.ZERO to Vec2d(width, height)
+
+ grow(4)
+ putQuad(
+ vec2(pos1.x, pos1.y).vec2(0.0, 1.0).end(),
+ vec2(pos1.x, pos2.y).vec2(0.0, 0.0).end(),
+ vec2(pos2.x, pos2.y).vec2(1.0, 0.0).end(),
+ vec2(pos2.x, pos1.y).vec2(1.0, 1.0).end()
+ )
+
+ shader.use()
+ shader["u_TexelSize"] = Vec2d.ONE / pos2
+ super.bind(0)
+
+ immediateDraw()
+ }
+ }
+
+ override fun bind(slot: Int) {
+ frame.bind(slot)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt
index f1883acdb..073108079 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/FilledRectRenderer.kt
@@ -18,23 +18,27 @@
package com.lambda.graphics.renderer.gui.rect
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
-import com.lambda.graphics.shader.Shader
-import com.lambda.util.math.MathUtils.toInt
+import com.lambda.graphics.renderer.gui.AbstractGUIRenderer
+import com.lambda.graphics.shader.Shader.Companion.shader
import com.lambda.util.math.Rect
import java.awt.Color
import kotlin.math.min
-class FilledRectRenderer : AbstractRectRenderer(
- VertexAttrib.Group.RECT_FILLED, shader
+object FilledRectRenderer : AbstractGUIRenderer(
+ VertexAttrib.Group.RECT_FILLED, shader("renderer/rect_filled")
) {
- fun build(
+ private const val MIN_SIZE = 0.5
+ private const val MIN_ALPHA = 1
+ private const val EXPAND = 0.3
+
+ fun filledRect(
rect: Rect,
roundRadius: Double = 0.0,
color: Color = Color.WHITE,
shade: Boolean = false,
- ) = build(rect, roundRadius, color, color, color, color, shade)
+ ) = filledRect(rect, roundRadius, color, color, color, color, shade)
- fun build(
+ fun filledRect(
rect: Rect,
roundRadius: Double = 0.0,
leftTop: Color = Color.WHITE,
@@ -42,7 +46,40 @@ class FilledRectRenderer : AbstractRectRenderer(
rightBottom: Color = Color.WHITE,
leftBottom: Color = Color.WHITE,
shade: Boolean = false,
- ) = pipeline.use {
+ ) = filledRect(
+ rect,
+ roundRadius, roundRadius, roundRadius, roundRadius,
+ leftTop, rightTop, rightBottom, leftBottom,
+ shade
+ )
+
+ fun filledRect(
+ rect: Rect,
+ leftTopRadius: Double = 0.0,
+ rightTopRadius: Double = 0.0,
+ rightBottomRadius: Double = 0.0,
+ leftBottomRadius: Double = 0.0,
+ color: Color = Color.WHITE,
+ shade: Boolean = false,
+ ) = filledRect(
+ rect,
+ leftTopRadius, rightTopRadius, rightBottomRadius, leftBottomRadius,
+ color, color, color, color,
+ shade
+ )
+
+ fun filledRect(
+ rect: Rect,
+ leftTopRadius: Double = 0.0,
+ rightTopRadius: Double = 0.0,
+ rightBottomRadius: Double = 0.0,
+ leftBottomRadius: Double = 0.0,
+ leftTop: Color = Color.WHITE,
+ rightTop: Color = Color.WHITE,
+ rightBottom: Color = Color.WHITE,
+ leftBottom: Color = Color.WHITE,
+ shade: Boolean = false,
+ ) = render(shade) {
val pos1 = rect.leftTop
val pos2 = rect.rightBottom
@@ -52,32 +89,33 @@ class FilledRectRenderer : AbstractRectRenderer(
rightTop.alpha < MIN_ALPHA &&
rightBottom.alpha < MIN_ALPHA &&
leftBottom.alpha < MIN_ALPHA
- ) return@use
- if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@use
+ ) return@render
+
+ if (size.x < MIN_SIZE || size.y < MIN_SIZE) return@render
val halfSize = size * 0.5
val maxRadius = min(halfSize.x, halfSize.y)
- val round = roundRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0)
+ val ltr = leftTopRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0)
+ val lbr = leftBottomRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0)
+ val rbr = rightBottomRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0)
+ val rtr = rightTopRadius.coerceAtMost(maxRadius).coerceAtLeast(0.0)
- val p1 = pos1 - 0.25
- val p2 = pos2 + 0.25
- val s = shade.toInt().toDouble()
+ val p1 = pos1 - EXPAND
+ val p2 = pos2 + EXPAND
- grow(4)
+ shader["u_Size"] = size
+ shader["u_RoundLeftTop"] = ltr
+ shader["u_RoundLeftBottom"] = lbr
+ shader["u_RoundRightBottom"] = rbr
+ shader["u_RoundRightTop"] = rtr
+ grow(4)
putQuad(
- vec2m(p1.x, p1.y).vec2(0.0, 0.0).vec2(size.x, size.y).float(round).float(s).color(leftTop).end(),
- vec2m(p1.x, p2.y).vec2(0.0, 1.0).vec2(size.x, size.y).float(round).float(s).color(leftBottom).end(),
- vec2m(p2.x, p2.y).vec2(1.0, 1.0).vec2(size.x, size.y).float(round).float(s).color(rightBottom).end(),
- vec2m(p2.x, p1.y).vec2(1.0, 0.0).vec2(size.x, size.y).float(round).float(s).color(rightTop).end()
+ vec3m(p1.x, p1.y, 0.0).vec2(0.0, 0.0).color(leftTop).end(),
+ vec3m(p1.x, p2.y, 0.0).vec2(0.0, 1.0).color(leftBottom).end(),
+ vec3m(p2.x, p2.y, 0.0).vec2(1.0, 1.0).color(rightBottom).end(),
+ vec3m(p2.x, p1.y, 0.0).vec2(1.0, 0.0).color(rightTop).end()
)
}
-
- companion object {
- private const val MIN_SIZE = 0.5
- private const val MIN_ALPHA = 3
-
- private val shader = Shader("renderer/rect_filled")
- }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt
index 5f758af13..a43381723 100644
--- a/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/renderer/gui/rect/OutlineRectRenderer.kt
@@ -19,32 +19,36 @@ package com.lambda.graphics.renderer.gui.rect
import com.lambda.graphics.buffer.IRenderContext
import com.lambda.graphics.buffer.vertex.attributes.VertexAttrib
+import com.lambda.graphics.pipeline.ScissorAdapter
+import com.lambda.graphics.renderer.gui.AbstractGUIRenderer
import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.shader.Shader.Companion.shader
import com.lambda.util.math.lerp
import com.lambda.util.math.MathUtils.toInt
import com.lambda.util.math.MathUtils.toRadian
import com.lambda.util.math.Rect
import com.lambda.util.math.Vec2d
+import com.lambda.util.math.transform
import java.awt.Color
import kotlin.math.cos
import kotlin.math.min
import kotlin.math.sin
-class OutlineRectRenderer : AbstractRectRenderer(
- VertexAttrib.Group.RECT_OUTLINE, shader
+object OutlineRectRenderer : AbstractGUIRenderer(
+ VertexAttrib.Group.RECT_OUTLINE, shader("renderer/rect_outline")
) {
- private val quality = 8
- private val verticesCount = quality * 4
+ private const val QUALITY = 8
+ private const val VERTICES_COUNT = QUALITY * 4
- fun build(
+ fun outlineRect(
rect: Rect,
roundRadius: Double = 0.0,
glowRadius: Double = 1.0,
color: Color = Color.WHITE,
shade: Boolean = false,
- ) = build(rect, roundRadius, glowRadius, color, color, color, color, shade)
+ ) = outlineRect(rect, roundRadius, glowRadius, color, color, color, color, shade)
- fun build(
+ fun outlineRect(
rect: Rect,
roundRadius: Double = 0.0,
glowRadius: Double = 1.0,
@@ -53,10 +57,10 @@ class OutlineRectRenderer : AbstractRectRenderer(
rightBottom: Color = Color.WHITE,
leftBottom: Color = Color.WHITE,
shade: Boolean = false,
- ) = pipeline.use {
- if (glowRadius < 1) return@use
+ ) = render(shade) {
+ if (glowRadius < 0.1) return@render
- grow(verticesCount * 3)
+ grow(VERTICES_COUNT * 3)
fun IRenderContext.genVertices(size: Double, isGlow: Boolean): MutableList {
val r = rect.expand(size)
@@ -66,15 +70,18 @@ class OutlineRectRenderer : AbstractRectRenderer(
val maxRadius = min(halfSize.x, halfSize.y) - 0.5
val round = (roundRadius + size).coerceAtMost(maxRadius).coerceAtLeast(0.0)
- fun MutableList.buildCorners(base: Vec2d, c: Color, angleRange: IntRange) = repeat(quality) {
+ fun MutableList.buildCorners(base: Vec2d, c: Color, angleRange: IntRange) = repeat(QUALITY) {
val min = angleRange.first.toDouble()
val max = angleRange.last.toDouble()
- val p = it.toDouble() / quality
+ val p = it.toDouble() / QUALITY
val angle = lerp(p, min, max).toRadian()
val pos = base + Vec2d(cos(angle), -sin(angle)) * round
- val s = shade.toInt().toDouble()
- add(vec2m(pos.x, pos.y).float(a).float(s).color(c).end())
+
+ val uvx = transform(pos.x, rect.left, rect.right, 0.0, 1.0)
+ val uvy = transform(pos.y, rect.top, rect.bottom, 0.0, 1.0)
+
+ add(vec3m(pos.x, pos.y, 0.0).vec2(uvx, uvy).float(a).color(c).end())
}
val rt = r.rightTop + Vec2d(-round, round)
@@ -94,7 +101,7 @@ class OutlineRectRenderer : AbstractRectRenderer(
fun drawStripWith(vertices: MutableList) {
var prev = main.last() to vertices.last()
- repeat(verticesCount) {
+ repeat(VERTICES_COUNT) {
val new = main[it] to vertices[it]
putQuad(new.first, new.second, prev.second, prev.first)
prev = new
@@ -104,8 +111,4 @@ class OutlineRectRenderer : AbstractRectRenderer(
drawStripWith(genVertices(-(glowRadius.coerceAtMost(1.0)), true))
drawStripWith(genVertices(glowRadius, true))
}
-
- companion object {
- private val shader = Shader("renderer/rect_outline")
- }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt
index 87ec7f5b1..c5b718a4d 100644
--- a/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/shader/Shader.kt
@@ -21,7 +21,6 @@ import com.lambda.graphics.RenderMain
import com.lambda.graphics.shader.ShaderUtils.createShaderProgram
import com.lambda.graphics.shader.ShaderUtils.loadShader
import com.lambda.graphics.shader.ShaderUtils.uniformMatrix
-import com.lambda.util.LambdaResource
import com.lambda.util.math.Vec2d
import it.unimi.dsi.fastutil.objects.Object2IntMap
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
@@ -30,19 +29,17 @@ import org.joml.Matrix4f
import org.lwjgl.opengl.GL20C.*
import java.awt.Color
-class Shader(fragmentPath: String, vertexPath: String) {
+class Shader private constructor(fragmentPath: String, vertexPath: String) {
private val uniformCache: Object2IntMap = Object2IntOpenHashMap()
private val id = createShaderProgram(
- loadShader(ShaderType.VERTEX_SHADER, LambdaResource("shaders/vertex/$vertexPath.vert")),
- loadShader(ShaderType.FRAGMENT_SHADER, LambdaResource("shaders/fragment/$fragmentPath.frag"))
+ loadShader(ShaderType.VERTEX_SHADER, "shaders/vertex/$vertexPath.vert"),
+ loadShader(ShaderType.FRAGMENT_SHADER, "shaders/fragment/$fragmentPath.frag")
)
- constructor(path: String) : this(path, path)
-
fun use() {
glUseProgram(id)
- set("u_ProjModel", Matrix4f(RenderMain.projectionMatrix).mul(RenderMain.modelViewMatrix))
+ set("u_ProjModel", RenderMain.projModel)
}
private fun loc(name: String) =
@@ -80,4 +77,16 @@ class Shader(fragmentPath: String, vertexPath: String) {
operator fun set(name: String, mat: Matrix4f) =
uniformMatrix(loc(name), mat)
+
+ companion object {
+ private val shaderCache = hashMapOf, Shader>()
+
+ fun shader(path: String) =
+ shader(path, path)
+
+ fun shader(fragmentPath: String, vertexPath: String) =
+ shaderCache.getOrPut(fragmentPath to vertexPath) {
+ Shader(fragmentPath, vertexPath)
+ }
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt
index 9e229ac46..6cfda3c32 100644
--- a/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/shader/ShaderUtils.kt
@@ -19,6 +19,7 @@ package com.lambda.graphics.shader
import com.google.common.collect.ImmutableList
import com.lambda.util.LambdaResource
+import com.lambda.util.stream
import com.mojang.blaze3d.platform.GlStateManager
import org.apache.commons.io.IOUtils
import org.joml.Matrix4f
@@ -42,7 +43,7 @@ object ShaderUtils {
error?.let { err ->
val builder = StringBuilder()
.append("Failed to compile ${type.name} shader").appendLine()
- .append("Path: ${resource.path}").appendLine()
+ .append("Path: $resource").appendLine()
.append("Compiler output:").appendLine()
.append(err)
@@ -52,10 +53,10 @@ object ShaderUtils {
return shader
}
- fun createShaderProgram(vert: Int, frag: Int): Int {
+ fun createShaderProgram(vararg shaders: Int): Int {
// Create new shader program
val program = glCreateProgram()
- val error = linkProgram(program, vert, frag)
+ val error = linkProgram(program, shaders)
// Handle error
error?.let { err ->
@@ -67,8 +68,7 @@ object ShaderUtils {
throw RuntimeException(builder.toString())
}
- glDeleteShader(vert)
- glDeleteShader(frag)
+ shaders.forEach(::glDeleteShader)
return program
}
@@ -81,9 +81,11 @@ object ShaderUtils {
else glGetShaderInfoLog(shader, shaderInfoLogLength)
}
- private fun linkProgram(program: Int, vertShader: Int, fragShader: Int): String? {
- glAttachShader(program, vertShader)
- glAttachShader(program, fragShader)
+ private fun linkProgram(program: Int, shaders: IntArray): String? {
+ shaders.forEach {
+ glAttachShader(program, it)
+ }
+
glLinkProgram(program)
val status = glGetProgrami(program, GL_LINK_STATUS)
diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt
new file mode 100644
index 000000000..6229c2c17
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/texture/AnimatedTexture.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.texture
+
+import com.lambda.graphics.buffer.pixel.PixelBuffer
+import com.lambda.util.Communication.logError
+import com.lambda.util.LambdaResource
+import com.lambda.util.stream
+import org.lwjgl.BufferUtils
+import org.lwjgl.stb.STBImage
+import java.nio.ByteBuffer
+
+
+class AnimatedTexture(path: LambdaResource) : Texture(image = null) {
+ private val pbo: PixelBuffer
+ private val gif: ByteBuffer // Do NOT free this pointer
+ private val frameDurations: IntArray
+ val channels: Int
+ val frames: Int
+
+ private val blockSize: Int
+ get() = width * height * channels
+
+ private var currentFrame = 0
+ private var lastUpload = 0L
+
+ override fun bind(slot: Int) {
+ update()
+ super.bind(slot)
+ }
+
+ fun update() {
+ if (System.currentTimeMillis() - lastUpload >= frameDurations[currentFrame]) {
+ // This is cool because instead of having a buffer for each frame we can
+ // just move the frame's block on each update
+ // 0 memory allocation and few cpu cycles
+ val slice = gif
+ .position(blockSize * currentFrame)
+ .limit(blockSize * (currentFrame + 1))
+
+ pbo.upload(slice, offset = 0)
+ ?.let { err -> logError("Error uploading to PBO", err) }
+
+ gif.clear()
+
+ currentFrame = (currentFrame + 1) % frames
+ lastUpload = System.currentTimeMillis()
+ }
+ }
+
+ init {
+ val bytes = path.stream.readAllBytes()
+ val buffer = ByteBuffer.allocateDirect(bytes.size)
+
+ buffer.put(bytes)
+ buffer.flip()
+
+ val pDelays = BufferUtils.createPointerBuffer(1)
+ val pWidth = BufferUtils.createIntBuffer(1)
+ val pHeight = BufferUtils.createIntBuffer(1)
+ val pLayers = BufferUtils.createIntBuffer(1)
+ val pChannels = BufferUtils.createIntBuffer(1)
+
+ // The buffer contains packed frames that can be extracted as follows:
+ // limit = width * height * channels * [frame number]
+ gif = STBImage.stbi_load_gif_from_memory(buffer, pDelays, pWidth, pHeight, pLayers, pChannels, 4)
+ ?: throw IllegalStateException("There was an unknown error while loading the gif file")
+
+ initialized = true
+ width = pWidth.get()
+ height = pHeight.get()
+ frames = pLayers.get()
+ channels = pChannels.get()
+ frameDurations = IntArray(frames)
+
+ pDelays.getIntBuffer(frames).get(frameDurations)
+
+ pbo = PixelBuffer(this@AnimatedTexture)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt
deleted file mode 100644
index 9d9109942..000000000
--- a/common/src/main/kotlin/com/lambda/graphics/texture/MipmapTexture.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.graphics.texture
-
-import com.lambda.graphics.texture.TextureUtils.rescale
-import com.lambda.graphics.texture.TextureUtils.setupLOD
-import com.lambda.graphics.texture.TextureUtils.upload
-import com.lambda.util.LambdaResource
-import org.lwjgl.opengl.GL14.*
-import java.awt.image.BufferedImage
-import javax.imageio.ImageIO
-
-class MipmapTexture(image: BufferedImage, levels: Int = 4) : Texture() {
- private var lastLod: Float? = null
-
- init {
- bind()
- setupLOD(levels)
-
- // Upload base image
- upload(image, 0)
-
- // Upload downscaled ones
- for (level in 1..levels) {
- val newWidth = image.width shr level
- val newHeight = image.height shr level
- val scaled = image.rescale(newWidth, newHeight)
-
- upload(scaled, level)
- }
- }
-
- fun setLOD(targetLod: Float) {
- if (lastLod == targetLod) return
- lastLod = targetLod
-
- glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, targetLod)
- }
-
- companion object {
- /**
- * Retrieves an image from the resources folder and generates a mipmap texture.
- *
- * @param path The path to the image.
- * @param levels The number of mipmap levels.
- */
- fun fromResource(path: String, levels: Int = 4): MipmapTexture =
- MipmapTexture(ImageIO.read(LambdaResource(path).stream), levels)
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt
index 52dc5b722..fe56a853e 100644
--- a/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/texture/Texture.kt
@@ -18,10 +18,193 @@
package com.lambda.graphics.texture
import com.lambda.graphics.texture.TextureUtils.bindTexture
+import com.lambda.graphics.texture.TextureUtils.readImage
+import com.lambda.graphics.texture.TextureUtils.setupTexture
+import net.minecraft.client.texture.NativeImage
import org.lwjgl.opengl.GL45C.*
+import java.awt.image.BufferedImage
+import java.awt.image.BufferedImage.*
+import java.nio.ByteBuffer
+import kotlin.IllegalStateException
+/**
+ * Represents a texture that can be uploaded and bound to the graphics pipeline
+ * Supports mipmap generation and LOD (Level of Detail) configuration
+ */
open class Texture {
+ val format: Int
+ private val levels: Int
+ private val nativeFormat: NativeImage.Format // For mojang native images
+
+ /**
+ * @param image Optional initial image to upload to the texture
+ * @param format The format of the image passed in, if the [image] is null, then you must pass the appropriate format
+ * @param levels Number of mipmap levels to generate for the texture
+ */
+ constructor(image: BufferedImage?, format: Int = GL_RGBA, levels: Int = 4) {
+ this.format = image?.type?.let { bufferedMapping[it] } ?: format
+ this.levels = levels
+ this.nativeFormat = nativeMapping.getOrDefault(format, NativeImage.Format.RGBA)
+
+ image?.let { bindTexture(id); upload(it) }
+ }
+
+ /**
+ * @param buffer The image buffer
+ * @param width The width of the image
+ * @param height The height of the image
+ * @param format The format of the image passed in, must be specified
+ * @param levels Number of mipmap levels to generate for the texture
+ */
+ constructor(buffer: ByteBuffer, width: Int, height: Int, format: Int, levels: Int = 4) {
+ this.format = format
+ this.levels = levels
+ this.nativeFormat = nativeMapping.getOrDefault(format, NativeImage.Format.RGBA)
+
+ bindTexture(id)
+ upload(buffer, width, height)
+ }
+
+ /**
+ * Indicates whether there is an initial texture or not
+ */
+ var initialized: Boolean = false; protected set
val id = glGenTextures()
- fun bind(slot: Int = 0) = bindTexture(id, slot)
+ var width = -1; protected set
+ var height = -1; protected set
+
+ /**
+ * Binds this texture to the specified slot in the graphics pipeline
+ */
+ open fun bind(slot: Int = 0) {
+ bindTexture(id, slot)
+ }
+
+ /**
+ * Unbinds any texture from the specified slot
+ */
+ open fun unbind(slot: Int = 0) {
+ bindTexture(0, slot)
+ }
+
+ /**
+ * Uploads an image to the texture and generates mipmaps for the texture if applicable
+ *
+ * Note that the texture must be bound before calling this function
+ *
+ * @param image The image to upload to the texture
+ * @param offset The mipmap level to upload the image to
+ */
+ fun upload(image: BufferedImage, offset: Int = 0) {
+ // Store level_base +1 through `level` images and generate
+ // mipmaps from them
+ setupLOD(levels)
+
+ width = image.width
+ height = image.height
+ initialized = true
+
+ // Set this mipmap to `offset` to define the original texture
+ setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)
+ glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, readImage(image, nativeFormat))
+ if (levels > 0) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack
+ }
+
+ /**
+ * Uploads an image to the texture and generates mipmaps for the texture if applicable
+ *
+ * Note that the texture must be bound before calling this function
+ *
+ * @param buffer The image buffer to upload to the texture
+ * @param width The width of the texture
+ * @param height The height of the texture
+ * @param offset The mipmap level to upload the image to
+ */
+ fun upload(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) {
+ // Store level_base +1 through `level` images and generate
+ // mipmaps from them
+ setupLOD(levels)
+
+ this.width = width
+ this.height = height
+ initialized = true
+
+ // Set this mipmap to `offset` to define the original texture
+ setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)
+ glTexImage2D(GL_TEXTURE_2D, offset, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, buffer)
+ if (levels > 0) glGenerateMipmap(GL_TEXTURE_2D) // This take the derived values GL_TEXTURE_BASE_LEVEL and GL_TEXTURE_MAX_LEVEL to generate the stack
+ }
+
+ /**
+ * Updates the data of a texture
+ *
+ * Note that the texture must be bound before calling this function
+ *
+ * @param image The image to upload to the texture
+ * @param offset The mipmap level to upload the image to
+ *
+ * @throws IllegalStateException If the texture has the consistency flag and is already initialized
+ */
+ fun update(image: BufferedImage, offset: Int = 0) {
+ if (!initialized) return upload(image, offset)
+
+ checkDimensions(width, height)
+ glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, readImage(image, nativeFormat))
+ }
+
+ /**
+ * Updates the data of a texture
+ * This function does not bind the texture
+ *
+ * @param buffer The image buffer to upload to the texture
+ * @param width The width of the texture
+ * @param height The height of the texture
+ * @param offset The mipmap level to upload the image to
+ *
+ * @throws IllegalStateException If the texture has the consistency flag and is already initialized
+ */
+ fun update(buffer: ByteBuffer, width: Int, height: Int, offset: Int = 0) {
+ if (!initialized) return upload(buffer, width, height, offset)
+
+ checkDimensions(width, height)
+ glTexSubImage2D(GL_TEXTURE_2D, offset, 0, 0, width, height, format, GL_UNSIGNED_BYTE, buffer)
+ }
+
+ private fun setupLOD(levels: Int) {
+ // When you call glTextureStorage, you're specifying the total number of levels, including level 0
+ // This is a 0-based index system, which means that the maximum mipmap level is n-1
+ //
+ // TLDR: This will not work correctly with immutable texture storage
+
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, levels)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0)
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels)
+ }
+
+ private fun checkDimensions(width: Int, height: Int) =
+ check(width + height <= this.width + this.height && initialized) {
+ "Client tried to update a texture with more data than allowed\n" +
+ "Expected ${this.width + this.height} bytes but got ${width + height}"
+ }
+
+ companion object {
+ private val nativeMapping = mapOf(
+ GL_RED to NativeImage.Format.LUMINANCE,
+ GL_GREEN to NativeImage.Format.LUMINANCE,
+ GL_BLUE to NativeImage.Format.LUMINANCE,
+ GL_RG to NativeImage.Format.LUMINANCE_ALPHA,
+ GL_RGB to NativeImage.Format.RGB,
+ GL_RGBA to NativeImage.Format.RGBA,
+ )
+
+ private val bufferedMapping = mapOf(
+ TYPE_BYTE_BINARY to GL_RED,
+ TYPE_BYTE_GRAY to GL_RG,
+ TYPE_INT_RGB to GL_RGB,
+ TYPE_INT_ARGB to GL_RGBA,
+ TYPE_4BYTE_ABGR to GL_BGRA,
+ )
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt
new file mode 100644
index 000000000..98f51465b
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureOwner.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.graphics.texture
+
+import com.lambda.graphics.renderer.gui.font.sdf.DistanceFieldTexture
+import com.lambda.util.readImage
+import java.awt.image.BufferedImage
+
+/**
+ * The [TextureOwner] object is responsible for managing textures owned by various objects in the render pipeline
+ */
+object TextureOwner {
+ private val textureMap = HashMap>()
+
+ /**
+ * Retrieves the first texture owned by the object
+ */
+ val Any.texture: Texture
+ get() = textureMap.getValue(this@texture)[0]
+
+ /**
+ * Retrieves the texture associated with the receiver object at the specified index
+ *
+ * @param index The index of the texture to retrieve
+ * @return The texture [T] at the given index
+ */
+ @Suppress("unchecked_cast")
+ fun Any.texture(index: Int) =
+ textureMap.getValue(this@texture)[index] as T
+
+ /**
+ * Binds a list of textures to texture slots, ensuring no more than 32 textures
+ * are bound at once (to fit within the typical GPU limitations)
+ *
+ * @param textures The list of objects that own textures to be bound.
+ * @throws IllegalArgumentException If more than 32 textures are provided.
+ */
+ fun bind(vararg textures: Any) {
+ check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" }
+
+ textures.forEachIndexed { index, texture -> texture.texture.bind(index) }
+ }
+
+ /**
+ * Binds a list of textures to texture slots, ensuring no more than 32 textures
+ * are bound at once (to fit within the typical GPU limitations)
+ *
+ * @param textures The list of textures to be bound
+ * @throws IllegalArgumentException If more than 32 textures are provided
+ */
+ fun bind(vararg textures: Texture) {
+ check(textures.size < 33) { "Texture slot overflow, expected to use less than 33 slots, got ${textures.size} slots" }
+
+ textures.forEachIndexed { index, texture -> texture.bind(index) }
+ }
+
+ /**
+ * Uploads a texture from image data and associates it with the object,
+ * optionally generating mipmaps for the texture
+ *
+ * @param data The image data as a [BufferedImage] to create the texture
+ * @param mipmaps The number of mipmaps to generate for the texture (default is 1)
+ * @return The created texture object
+ */
+ fun Any.upload(data: BufferedImage, mipmaps: Int = 1) =
+ Texture(data, levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) }
+
+ /**
+ * Uploads a texture from an image file path and associates it with the object,
+ * optionally generating mipmaps for the texture
+ *
+ * @param path The resource path to the image file
+ * @param mipmaps The number of mipmaps to generate for the texture (default is 1)
+ * @return The created texture object
+ */
+ fun Any.upload(path: String, mipmaps: Int = 1) =
+ Texture(path.readImage(), levels = mipmaps).also { textureMap.computeIfAbsent(this@upload) { mutableListOf() }.add(it) }
+
+ /**
+ * Uploads a distance field texture from image data and associates it with the object
+ * Distance field textures are commonly used for rendering fonts.
+ *
+ * @param data The image data as a [BufferedImage] to create the distance field texture
+ * @return The created distance field texture object
+ */
+ fun Any.uploadField(data: BufferedImage) =
+ DistanceFieldTexture(data).also { textureMap.computeIfAbsent(this@uploadField) { mutableListOf() }.add(it) }
+
+ /**
+ * Uploads a GIF and associates it with the object as an animated texture
+ *
+ * @param path The resource path to the GIF file
+ * @return The created animated texture object
+ */
+ fun Any.uploadGif(path: String) =
+ AnimatedTexture(path).also { textureMap.computeIfAbsent(this@uploadGif) { mutableListOf() }.add(it) }
+}
diff --git a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt
index 2fe308fbc..16eca5626 100644
--- a/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt
+++ b/common/src/main/kotlin/com/lambda/graphics/texture/TextureUtils.kt
@@ -22,18 +22,13 @@ import com.pngencoder.PngEncoder
import net.minecraft.client.texture.NativeImage
import org.lwjgl.BufferUtils
import org.lwjgl.opengl.GL45C.*
-import java.awt.*
import java.awt.image.BufferedImage
import java.nio.ByteBuffer
-import kotlin.math.roundToInt
-import kotlin.math.sqrt
object TextureUtils {
- private const val COMPRESSION_LEVEL = 1
+ private const val COMPRESSION_LEVEL = -1
private const val THREADED_COMPRESSION = false
- private val metricCache = mutableMapOf()
-
val encoderPreset = PngEncoder()
.withCompressionLevel(COMPRESSION_LEVEL)
.withMultiThreadedCompressionEnabled(THREADED_COMPRESSION)
@@ -43,22 +38,6 @@ object TextureUtils {
RenderSystem.bindTexture(id)
}
- fun upload(bufferedImage: BufferedImage, lod: Int) {
- val width = bufferedImage.width
- val height = bufferedImage.height
-
- glTexImage2D(GL_TEXTURE_2D, lod, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, readImage(bufferedImage))
-
- setupTexture(GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR)
- }
-
- fun setupLOD(levels: Int) {
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, levels)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0)
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, levels)
- }
-
fun setupTexture(minFilter: Int, magFilter: Int) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter)
@@ -73,7 +52,7 @@ object TextureUtils {
fun readImage(
bufferedImage: BufferedImage,
- format: NativeImage.Format = NativeImage.Format.RGBA,
+ format: NativeImage.Format,
): Long {
val bytes = encoderPreset
.withBufferedImage(bufferedImage)
@@ -89,71 +68,6 @@ object TextureUtils {
fun readImage(
image: ByteBuffer,
- format: NativeImage.Format = NativeImage.Format.RGBA,
+ format: NativeImage.Format,
) = NativeImage.read(format, image).pointer
-
- fun getCharImage(font: Font, codePoint: Char): BufferedImage? {
- if (!font.canDisplay(codePoint)) return null
-
- val fontMetrics = metricCache.getOrPut(font) {
- val image = BufferedImage(COMPRESSION_LEVEL, COMPRESSION_LEVEL, BufferedImage.TYPE_INT_ARGB)
- val graphics2D = image.createGraphics()
-
- graphics2D.font = font
- graphics2D.dispose()
-
- image.graphics.getFontMetrics(font)
- }
-
- val charWidth = if (fontMetrics.charWidth(codePoint) > 0) fontMetrics.charWidth(codePoint) else 8
- val charHeight = if (fontMetrics.height > 0) fontMetrics.height else font.size
-
- val charImage = BufferedImage(charWidth, charHeight, BufferedImage.TYPE_INT_ARGB)
- val graphics2D = charImage.createGraphics()
-
- graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
- graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT)
-
- graphics2D.font = font
- graphics2D.color = Color.WHITE
- graphics2D.drawString(codePoint.toString(), 0, fontMetrics.ascent)
- graphics2D.dispose()
-
- return charImage
- }
-
- fun BufferedImage.rescale(targetWidth: Int, targetHeight: Int): BufferedImage {
- val type = if (transparency == Transparency.OPAQUE)
- BufferedImage.TYPE_INT_RGB
- else BufferedImage.TYPE_INT_ARGB
-
- var image = this
-
- var width = image.width
- var height = image.height
-
- val divisorX = sqrt((width / targetWidth).toDouble())
- val divisorY = sqrt((height / targetHeight).toDouble())
-
- do {
- if (width > targetWidth) {
- width = (width / divisorX).roundToInt().coerceAtLeast(targetWidth)
- }
-
- if (height > targetHeight) {
- height = (height / divisorY).roundToInt().coerceAtLeast(targetHeight)
- }
-
- val tempImage = BufferedImage(width, height, type)
- val graphics2D = tempImage.createGraphics()
-
- graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR)
- graphics2D.drawImage(image, 0, 0, width, height, null)
- graphics2D.dispose()
-
- image = tempImage
- } while (width != targetWidth || height != targetHeight)
-
- return image
- }
}
diff --git a/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt
deleted file mode 100644
index 0b4f69c0f..000000000
--- a/common/src/main/kotlin/com/lambda/gui/AbstractGuiConfigurable.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui
-
-import com.lambda.config.Configurable
-import com.lambda.config.configurations.GuiConfig
-import com.lambda.core.Loadable
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.windows.tag.TagWindow
-import com.lambda.module.tag.ModuleTag
-import com.lambda.util.math.Vec2d
-
-abstract class AbstractGuiConfigurable(
- private val ownerGui: AbstractClickGui,
- private val tags: Set,
- override val name: String
-) : Configurable(GuiConfig), Loadable {
- var mainWindows by setting("windows", defaultWindows)
-
- private val defaultWindows
- get() =
- tags.mapIndexed { index, tag ->
- TagWindow(tag, ownerGui).apply {
- val step = 5.0
- position = Vec2d((width + step) * index, 0.0) + step
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt
new file mode 100644
index 000000000..822a8e3f2
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui
+
+import com.lambda.config.settings.NumericSetting
+import com.lambda.config.settings.comparable.BooleanSetting
+import com.lambda.config.settings.comparable.EnumSetting
+import com.lambda.config.settings.FunctionSetting
+import com.lambda.config.settings.complex.KeyBindSetting
+import com.lambda.core.Loadable
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.settings.BooleanButton.Companion.booleanSetting
+import com.lambda.gui.impl.clickgui.module.settings.EnumSlider.Companion.enumSetting
+import com.lambda.gui.impl.clickgui.module.settings.KeybindPicker.Companion.keybindSetting
+import com.lambda.gui.impl.clickgui.module.settings.NumberSlider.Companion.numericSetting
+import com.lambda.gui.impl.clickgui.settings.UnitButton.Companion.unitSetting
+import kotlin.reflect.KClass
+
+object GuiManager : Loadable {
+ val typeMap = mutableMapOf, (owner: Layout, converted: Any) -> Layout>()
+
+ private inline fun typeAdapter(noinline block: (Layout, T) -> Layout) {
+ typeMap[T::class] = { owner, converted -> block(owner, converted as T) }
+ }
+
+ override fun load(): String {
+ typeAdapter { owner, ref ->
+ owner.booleanSetting(ref)
+ }
+
+ typeAdapter> { owner, ref ->
+ owner.enumSetting(ref)
+ }
+
+ typeAdapter> { owner, ref ->
+ owner.unitSetting(ref)
+ }
+
+ typeAdapter> { owner, ref ->
+ owner.numericSetting(ref)
+ }
+
+ typeAdapter { owner, ref ->
+ owner.keybindSetting(ref)
+ }
+
+ return "Loaded ${typeMap.size} gui type adapters."
+ }
+
+ /**
+ * Attempts to convert the given [reference] to the [Layout]
+ */
+ @UIBuilder
+ inline fun Layout.layoutOf(
+ reference: Any,
+ block: Layout.() -> Unit = {}
+ ): Layout? =
+ (typeMap[reference::class] ?: typeMap.entries.firstOrNull {
+ reference::class.java.superclass == it.key.java
+ }?.value)?.invoke(this, reference)?.apply(block)
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt
new file mode 100644
index 000000000..cf9a58d83
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/LambdaScreen.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui
+
+import com.lambda.Lambda.mc
+import com.lambda.event.Muteable
+import com.lambda.event.events.RenderEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.event.events.GuiEvent
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.KeyCode
+import com.lambda.util.Mouse
+import com.lambda.util.Nameable
+import com.lambda.util.math.Vec2d
+import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.text.Text
+
+/**
+ * Represents a "tunnel" between the [Layout] system and minecraft's [Screen]
+ */
+class LambdaScreen(
+ override val name: String,
+ val layout: ScreenLayout
+) : Screen(Text.of(name)), Nameable, Muteable {
+ override val isMuted: Boolean get() = !isOpen
+
+ private var screenSize = Vec2d.ZERO
+ val isOpen get() = mc.currentScreen == this
+
+ init {
+ listen { event ->
+ screenSize = event.screenSize
+ layout.onEvent(GuiEvent.Update)
+ layout.onEvent(GuiEvent.Render)
+ }
+
+ listen {
+ layout.onEvent(GuiEvent.Tick)
+ }
+ }
+
+ fun show() {
+ mc.currentScreen?.close()
+
+ recordRenderCall {
+ mc.setScreen(this)
+ }
+ }
+
+ override fun onDisplayed() {
+ layout.onEvent(GuiEvent.Show)
+ }
+
+ override fun removed() {
+ layout.onEvent(GuiEvent.Hide)
+ }
+
+ override fun shouldPause() = false
+
+ override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, delta: Float) {
+ // Let's remove background tint
+ }
+
+ override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ val translated = KeyCode.virtualMapUS(keyCode, scanCode)
+ layout.onEvent(GuiEvent.KeyPress(translated))
+
+ if (keyCode == KeyCode.ESCAPE.keyCode) {
+ close()
+ }
+
+ return true
+ }
+
+ override fun charTyped(chr: Char, modifiers: Int): Boolean {
+ layout.onEvent(GuiEvent.CharTyped(chr))
+ return true
+ }
+
+ override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ layout.onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY)))
+ return true
+ }
+
+ override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
+ layout.onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY)))
+ return true
+ }
+
+ override fun mouseMoved(mouseX: Double, mouseY: Double) {
+ layout.onEvent(GuiEvent.MouseMove(rescaleMouse(mouseX, mouseY)))
+ }
+
+ override fun mouseScrolled(
+ mouseX: Double,
+ mouseY: Double,
+ horizontalAmount: Double,
+ verticalAmount: Double
+ ): Boolean {
+ layout.onEvent(GuiEvent.MouseScroll(rescaleMouse(mouseX, mouseY), verticalAmount))
+ return true
+ }
+
+ private fun rescaleMouse(mouseX: Double, mouseY: Double): Vec2d {
+ val mcMouse = Vec2d(mouseX, mouseY)
+ val mcWindow = Vec2d(mc.window.scaledWidth, mc.window.scaledHeight)
+
+ val uv = mcMouse / mcWindow
+ return uv * screenSize
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt
new file mode 100644
index 000000000..07c642ebc
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/ScreenLayout.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui
+
+import com.lambda.graphics.RenderMain
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+
+class ScreenLayout : Layout(owner = null) {
+ init {
+ onUpdate {
+ size = RenderMain.screenSize
+ }
+ }
+
+ companion object {
+ /**
+ * Creates gui layout
+ */
+ @UIBuilder
+ fun gui(name: String, block: ScreenLayout.() -> Unit) =
+ LambdaScreen(name, ScreenLayout().apply(block))
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt b/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt
deleted file mode 100644
index 21b421cce..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/LambdaGui.kt
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api
-
-import com.lambda.Lambda.mc
-import com.lambda.event.Muteable
-import com.lambda.event.events.ClientEvent
-import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.graphics.animation.AnimationTicker
-import com.lambda.gui.api.component.core.IComponent
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.module.Module
-import com.lambda.threading.runSafe
-import com.lambda.util.KeyCode
-import com.lambda.util.Mouse
-import com.lambda.util.Nameable
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Vec2d
-import com.mojang.blaze3d.systems.RenderSystem.recordRenderCall
-import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
-
-abstract class LambdaGui(
- override val name: String,
- private val owner: Module? = null
-) : Screen(Text.of(name)), IComponent, Nameable, Muteable {
- var screenSize = Vec2d.ZERO
- override val rect get() = Rect(Vec2d.ZERO, screenSize)
-
- val isOpen get() = mc.currentScreen == this
- override val isMuted: Boolean get() = !isOpen
- private var closingAction: (() -> Unit)? = null
-
- val animation = AnimationTicker()
-
- init {
- listen { event ->
- screenSize = event.screenSize
- onEvent(GuiEvent.Render())
- }
-
- listen {
- animation.tick()
- onEvent(GuiEvent.Tick())
- }
- }
-
- /**
- * Shows this gui screen
- *
- * No safe context required (TODO: let user open clickgui via main menu)
- */
- fun show() {
- owner?.enable()
- if (isOpen) return
-
- when (val screen = mc.currentScreen) {
- is AbstractClickGui -> {
- screen.close()
-
- screen.setCloseTask {
- mc.setScreen(this)
- }
- }
-
- else -> {
- screen?.close()
-
- recordRenderCall {
- mc.setScreen(this)
- }
- }
- }
- }
-
- final override fun onDisplayed() {
- onEvent(GuiEvent.Show())
- }
-
- override fun removed() {
- onEvent(GuiEvent.Hide())
-
- runSafe {
- // quick crashfix (is there any other way to prevent gui being closed twice?)
- mc.currentScreen = null
- owner?.disable()
- mc.currentScreen = this@LambdaGui
-
- closingAction?.let {
- recordRenderCall(it)
- closingAction = null
- }
- }
- }
-
- fun setCloseTask(block: () -> Unit) {
- closingAction = block
- }
-
- final override fun render(context: DrawContext?, mouseX: Int, mouseY: Int, delta: Float) {
- // Let's remove background tint
- }
-
- final override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- val translated = KeyCode.virtualMapUS(keyCode, scanCode)
- onEvent(GuiEvent.KeyPress(translated))
-
- if (keyCode == KeyCode.ESCAPE.keyCode) {
- close()
- }
-
- return true
- }
-
- final override fun charTyped(chr: Char, modifiers: Int): Boolean {
- onEvent(GuiEvent.CharTyped(chr))
- return true
- }
-
- final override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
- onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Click, rescaleMouse(mouseX, mouseY)))
- return true
- }
-
- final override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
- onEvent(GuiEvent.MouseClick(Mouse.Button.fromMouseCode(button), Mouse.Action.Release, rescaleMouse(mouseX, mouseY)))
- return true
- }
-
- final override fun mouseMoved(mouseX: Double, mouseY: Double) {
- onEvent(GuiEvent.MouseMove(rescaleMouse(mouseX, mouseY)))
- }
-
- override fun mouseScrolled(
- mouseX: Double,
- mouseY: Double,
- horizontalAmount: Double,
- verticalAmount: Double
- ): Boolean {
- onEvent(GuiEvent.MouseScroll(rescaleMouse(mouseX, mouseY), verticalAmount))
- return true
- }
-
- final override fun shouldPause() = false
-
- private fun rescaleMouse(mouseX: Double, mouseY: Double): Vec2d {
- val mcMouse = Vec2d(mouseX, mouseY)
- val mcWindow = Vec2d(mc.window.scaledWidth, mc.window.scaledHeight)
-
- val uv = mcMouse / mcWindow
- return uv * screenSize
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt
deleted file mode 100644
index 4b852fc90..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/RenderLayer.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api
-
-import com.lambda.graphics.renderer.gui.font.FontRenderer
-import com.lambda.graphics.renderer.gui.font.LambdaEmoji
-import com.lambda.graphics.renderer.gui.font.LambdaFont
-import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer
-import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer
-import com.lambda.threading.mainThread
-
-class RenderLayer {
- val filled by mainThread(::FilledRectRenderer)
- val outline by mainThread(::OutlineRectRenderer)
- val font by mainThread {
- FontRenderer(
- LambdaFont.FiraSansRegular,
- LambdaEmoji.Twemoji,
- )
- }
-
- fun render() {
- filled.render()
- outline.render()
- font.render()
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt
deleted file mode 100644
index ec4db511e..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/InteractiveComponent.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component
-
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.IComponent
-import com.lambda.util.Mouse
-import com.lambda.util.math.Vec2d
-
-abstract class InteractiveComponent : IComponent {
- protected open val hovered get() = rect.contains(lastMouse)
- protected var activeButton: Mouse.Button? = null
-
- protected open fun onPress(e: GuiEvent.MouseClick) {}
- protected open fun onRelease(e: GuiEvent.MouseClick) {}
-
- private var lastMouse = Vec2d.ZERO
-
- override fun onEvent(e: GuiEvent) {
- when (e) {
- is GuiEvent.Show -> {
- lastMouse = Vec2d.ONE * -1000.0
- activeButton = null
- }
-
- is GuiEvent.MouseMove -> {
- lastMouse = e.mouse
- }
-
- is GuiEvent.MouseClick -> {
- lastMouse = e.mouse
-
- val prevPressed = activeButton != null
- activeButton =
- if (hovered && e.button.isMainButton && e.action == Mouse.Action.Click) e.button else null
- val pressed = activeButton != null
-
- if (prevPressed == pressed) return
- if (pressed) onPress(e)
- else onRelease(e)
- }
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt b/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt
deleted file mode 100644
index 0ab6e22c0..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/ListWindow.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component
-
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.button.ListButton
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.module.modules.client.ClickGui
-
-abstract class ListWindow(
- owner: AbstractClickGui,
-) : WindowComponent(owner) {
- private var scrollOffset: Double = 0.0
- private var rubberbandRequest = 0.0
- private var rubberbandDelta = 0.0
-
- override fun onEvent(e: GuiEvent) {
- when (e) {
- is GuiEvent.Tick -> {
- rubberbandDelta += rubberbandRequest
- rubberbandRequest = 0.0
-
- rubberbandDelta *= 0.5
- if (rubberbandDelta < 0.05) rubberbandDelta = 0.0
-
- var y = scrollOffset + rubberbandDelta
- contentComponents.children.forEach { button ->
- button.heightOffset = y
- y += button.size.y + button.listStep
- }
- }
-
- is GuiEvent.MouseScroll -> {
- val delta = e.delta * 10.0 * ClickGui.scrollSpeed
- scrollOffset += delta
-
- val prevOffset = scrollOffset
- val range = -contentComponents.children.sumOf { it.size.y + it.listStep }
- scrollOffset = scrollOffset.coerceAtLeast(range).coerceAtMost(0.0)
-
- rubberbandRequest += prevOffset - scrollOffset
- }
- }
-
- super.onEvent(e)
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt
deleted file mode 100644
index 50592ee32..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/WindowComponent.kt
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component
-
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.graphics.gl.Scissor.scissor
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.core.DockingRect
-import com.lambda.gui.api.component.core.list.ChildComponent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.module.modules.client.GuiSettings.primaryColor
-import com.lambda.util.Mouse
-import com.lambda.util.math.MathUtils.toInt
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.setAlpha
-import java.awt.Color
-import kotlin.math.abs
-
-abstract class WindowComponent(
- val gui: AbstractClickGui,
-) : ChildComponent(gui.windows) {
- abstract val title: String
-
- abstract var width: Double
- abstract var height: Double
-
- var isOpen = true
- override val isActive get() = isOpen
-
- private var dragOffset: Vec2d? = null
- private val padding get() = ClickGui.windowPadding
-
- private val rectHandler = object : DockingRect() {
- override var relativePos = Vec2d.ZERO
- override val width get() = this@WindowComponent.width
- override val height get() = renderHeightAnimation + titleBarHeight
-
- override val dockingBase get() = titleBar.center
-
- override var allowHAlign = ClickGui.allowHAlign
- override var allowVAlign = ClickGui.allowVAlign
- }
-
- var serializedPosition by rectHandler::relativePos
- var position by rectHandler::position
- final override val rect by rectHandler::rect
-
- var dockingH by rectHandler::dockingH
- var dockingV by rectHandler::dockingV
-
- private val contentRect get() = rect.shrink(padding).moveFirst(Vec2d(0.0, titleBarHeight - padding))
-
- private val titleBar: Rect get() = Rect.basedOn(rect.leftTop, rect.size.x, titleBarHeight)
- private val titleBarHeight get() = ClickGui.buttonHeight * 1.25
-
- private val renderer = RenderLayer()
- private val contentRenderer = RenderLayer()
-
- private val animation = gui.animation
-
- private val showAnimation by animation.exp(0.0, 1.0, 0.6, ::isOpen)
- override val childShowAnimation get() = lerp(gui.childShowAnimation, 0.0, showAnimation)
-
- private val actualHeight get() = height + padding * 2 * isOpen.toInt()
- private var renderHeightAnimation by animation.exp({ 0.0 }, ::actualHeight, 0.6, ::isOpen)
-
- open val contentComponents = ChildLayer.Drawable>(gui, this, contentRenderer, ::contentRect)
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- when (e) {
- is GuiEvent.Show -> {
- dragOffset = null
- renderHeightAnimation = if (isOpen) actualHeight else 0.0
- }
-
- is GuiEvent.Render -> {
- updateRect()
-
- // TODO: fix blur
- // BlurPostProcessor.render(rect, ClickGui.windowBlur, guiAnimation)
-
- val alpha = (gui.childShowAnimation * 2.0).coerceIn(0.0, 1.0)
-
- // Background
- renderer.filled.build(
- rect = rect,
- roundRadius = ClickGui.windowRadius,
- color = GuiSettings.backgroundColor.multAlpha(alpha),
- shade = GuiSettings.shadeBackground
- )
-
- // Outline
- renderer.outline.build(
- rect = rect,
- roundRadius = ClickGui.windowRadius,
- glowRadius = ClickGui.glowRadius,
- color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(alpha),
- shade = GuiSettings.shadeBackground
- )
-
- // Title
- renderer.font.build(
- text = title,
- position = titleBar.center - Vec2d(renderer.font.getWidth(title) * 0.5, 0.0),
- color = Color.WHITE.setAlpha(gui.childShowAnimation)
- )
-
- renderer.render()
-
- scissor(contentRect) {
- contentComponents.onEvent(e)
- contentRenderer.render()
- }
-
- return
- }
-
- is GuiEvent.MouseMove -> {
- val prevPos = position
-
- dragOffset?.let {
- position = e.mouse - it
-
- if (prevPos != position) {
- rectHandler.autoDocking()
- }
- }
- }
-
- is GuiEvent.MouseClick -> {
- dragOffset = null
-
- if (e.mouse in titleBar && e.action == Mouse.Action.Click) {
- when (e.button) {
- Mouse.Button.Left -> dragOffset = e.mouse - position
- Mouse.Button.Right -> {
- // Don't let user spam
- val targetHeight = if (isOpen) actualHeight else 0.0
- if (abs(targetHeight - renderHeightAnimation) > 1) return
-
- isOpen = !isOpen
-
- if (isOpen) contentComponents.onEvent(GuiEvent.Show())
- }
-
- else -> {}
- }
- }
- }
- }
-
- contentComponents.onEvent(e)
- }
-
- private fun updateRect() = rectHandler.apply {
- screenSize = gui.screenSize
-
- var updateDocking = false
-
- if (allowHAlign != ClickGui.allowHAlign) {
- allowHAlign = ClickGui.allowHAlign
- updateDocking = true
- }
-
- if (allowVAlign != ClickGui.allowVAlign) {
- allowVAlign = ClickGui.allowVAlign
- updateDocking = true
- }
-
- if (updateDocking) autoDocking()
- }
-
- fun focus() {
- // move window into foreground
- gui.apply {
- scheduleAction {
- windows.children.apply {
- this@WindowComponent
- .apply(::remove)
- .apply(::add)
- }
- }
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt
deleted file mode 100644
index 645d4d803..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/button/ButtonComponent.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component.button
-
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.list.ChildComponent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.sound.LambdaSound
-import com.lambda.sound.SoundManager.playSoundRandomly
-import com.lambda.util.Mouse
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import java.awt.Color
-
-abstract class ButtonComponent(
- owner: ChildLayer.Drawable<*, *>,
-) : ChildComponent(owner) {
- abstract val position: Vec2d
- abstract val size: Vec2d
-
- abstract val text: String
- protected open val textColor
- get() = lerp(activeAnimation, Color.WHITE, GuiSettings.mainColor).multAlpha(
- showAnimation
- )
- protected open val centerText = false
-
- protected abstract var activeAnimation: Double
- protected open val roundRadius get() = ClickGui.buttonRadius
-
- private val actualSize get() = Vec2d(if (size.x == FILL_PARENT) owner.rect.size.x else size.x, size.y)
- final override val rect get() = Rect.basedOn(position, actualSize) + owner.rect.leftTop
-
- protected val renderer = owner.renderer
- protected val animation = owner.gui.animation
-
- private var hoverRectAnimation by animation.exp({ 0.0 }, { 1.0 }, { if (renderHovered) 0.6 else 0.07 }, ::renderHovered)
- protected var hoverFontAnimation by animation.exp(0.0, 1.0, 0.5, ::renderHovered)
- protected var pressAnimation by animation.exp(0.0, 1.0, 0.5) { activeButton != null }
- protected val interactAnimation get() = lerp(pressAnimation, hoverRectAnimation, 1.5) * 0.4
- override val childShowAnimation: Double get() = owner.childShowAnimation
- protected open val showAnimation get() = owner.childShowAnimation
-
- private var lastHoveredTime = 0L
- private val renderHovered get() = hovered || System.currentTimeMillis() - lastHoveredTime < 110
-
- // Removes button shrinking if there's no space between buttons
- protected val shrinkAnimation get() = lerp(ClickGui.buttonStep, 0.0, interactAnimation)
-
- open fun performClickAction(e: GuiEvent.MouseClick) {}
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- when (e) {
- is GuiEvent.Show, is GuiEvent.Hide -> reset()
-
- is GuiEvent.Render -> {
- // Active color
- renderer.filled.build(
- rect = rect.shrink(shrinkAnimation),
- roundRadius = roundRadius,
- color = GuiSettings.mainColor.multAlpha(activeAnimation * 0.3 * showAnimation),
- shade = GuiSettings.shade
- )
-
- // Hover glint
- val hoverRect = Rect.basedOn(rect.leftTop, rect.size.x * hoverRectAnimation, rect.size.y)
- renderer.filled.build(
- rect = hoverRect.shrink(shrinkAnimation),
- roundRadius = roundRadius,
- color = GuiSettings.mainColor.multAlpha(interactAnimation * 0.2 * showAnimation),
- shade = GuiSettings.shade
- )
-
- // Text
- val textScale = 1.0 - pressAnimation * 0.08
- val textX = ClickGui.windowPadding + interactAnimation + hoverFontAnimation
- val textXCentered = rect.size.x * 0.5 - renderer.font.getWidth(text, textScale) * 0.5
- renderer.font.build(
- text = text,
- position = Vec2d(rect.left + if (!centerText) textX else textXCentered, rect.center.y),
- color = textColor,
- scale = textScale
- )
- }
-
- is GuiEvent.MouseMove -> {
- val time = System.currentTimeMillis()
- if (hovered) lastHoveredTime = time
- }
- }
- }
-
- override fun onPress(e: GuiEvent.MouseClick) {
- val pitch = if (e.button == Mouse.Button.Left) 1.0 else 0.9
- playSoundRandomly(LambdaSound.BUTTON_CLICK.event, pitch)
- }
-
- override fun onRelease(e: GuiEvent.MouseClick) {
- if (hovered) performClickAction(e)
- }
-
- private fun reset() {
- activeAnimation = 0.0
- hoverRectAnimation = 0.0
- pressAnimation = 0.0
- lastHoveredTime = 0L
- }
-
- companion object {
- const val FILL_PARENT = -1.0
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt
deleted file mode 100644
index c5c7812ec..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/button/InputBarOverlay.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component.button
-
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.core.list.ChildComponent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.util.KeyCode
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.setAlpha
-import java.awt.Color
-import kotlin.math.abs
-
-abstract class InputBarOverlay(
- val renderer: RenderLayer,
- owner: ChildLayer.Drawable
-) : ChildComponent(owner) {
- override val rect: Rect get() = owner.rect
- override var isActive = false
-
- protected abstract val pressAnimation: Double
- protected abstract val interactAnimation: Double
- protected abstract val hoverFontAnimation: Double
- protected abstract val showAnimation: Double
- protected open val isKeyBind: Boolean = false
-
- val activeAnimation by owner.gui.animation.exp(0.0, 1.0, 0.7, ::isActive)
- private var typeAnimation by owner.gui.animation.exp({ 0.0 }, 0.2)
-
- private var targetOffset = 0.0
- private var offset by owner.gui.animation.exp(::targetOffset, 0.4)
-
- abstract fun getText(): String
- open fun setStringValue(string: String) {}
- open fun setKeyValue(key: KeyCode) {}
-
- open fun isCharAllowed(string: String, char: Char): Boolean = true
-
- private var typed = ""
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- when (e) {
- is GuiEvent.Show -> {
- isActive = false
- }
-
- is GuiEvent.Render -> {
- // Value text
- renderer.font.apply {
- val text = getText()
- val scale = lerp(1.0 - activeAnimation, 0.5, 1.0)
- val position =
- Vec2d(rect.right, rect.center.y) - Vec2d(ClickGui.windowPadding + getWidth(text, scale), 0.0)
- val color = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, 1.0 - activeAnimation))
-
- build(text, position, color, scale)
- }
-
- val textStartX = rect.left + ClickGui.windowPadding + interactAnimation + hoverFontAnimation
- val textColor = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, activeAnimation))
-
- // Typing field
- renderer.font.apply {
- val scale = lerp(activeAnimation, 0.5, 1.0) - pressAnimation * 0.08
- val position = Vec2d(textStartX, rect.center.y)
-
- targetOffset = getWidth(typed, scale)
- build(typed, position, textColor, scale)
- }
-
- // Separator
- renderer.filled.apply {
- val shrink = lerp(activeAnimation, rect.size.y * 0.5, 2 + abs(typeAnimation))
-
- val rect = Rect(
- Vec2d(0.0, rect.top + shrink),
- Vec2d(1.0, rect.bottom - shrink)
- ) + Vec2d(lerp(activeAnimation, rect.right, textStartX + offset + 2), 0.0)
-
- build(rect, color = textColor.multAlpha(0.8))
- }
- }
-
- is GuiEvent.CharTyped -> {
- if (!isActive || !isCharAllowed(typed, e.char) || isKeyBind) return
- typed += e.char
- typeAnimation = 1.0
- }
-
- is GuiEvent.KeyPress -> {
- if (!isActive) return
-
- if (isKeyBind) {
- val key = when (e.key) {
- KeyCode.DELETE, KeyCode.BACKSPACE -> KeyCode.UNBOUND
- KeyCode.ESCAPE -> return
- else -> e.key
- }
-
- setKeyValue(key)
- toggle()
- return
- }
-
- when (e.key) {
- KeyCode.ENTER -> {
- setStringValue(typed)
- toggle()
- }
-
- KeyCode.BACKSPACE -> {
- typed = typed.dropLast(1)
- typeAnimation = -1.0
- }
-
- else -> {}
- }
- }
- }
- }
-
- fun toggle() {
- isActive = !isActive
- if (isActive) typed = getText().filter { isCharAllowed("", it) }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt b/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt
deleted file mode 100644
index 4880fd119..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/button/ListButton.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component.button
-
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-
-abstract class ListButton(owner: ChildLayer.Drawable<*, *>) : ButtonComponent(owner) {
- override val position get() = Vec2d(0.0, lerp(owner.childShowAnimation, 0.0, renderHeightOffset))
- override val size get() = Vec2d(FILL_PARENT, ClickGui.buttonHeight)
-
- open val listStep get() = ClickGui.buttonStep
-
- var heightOffset = 0.0
- protected var renderHeightAnimation by animation.exp(::heightOffset, 0.8)
- protected open val renderHeightOffset get() = renderHeightAnimation
-
- override fun onEvent(e: GuiEvent) {
- if (e is GuiEvent.Show) {
- heightOffset = 0.0
- renderHeightAnimation = 0.0
- }
- super.onEvent(e)
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt b/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt
deleted file mode 100644
index 737eead08..000000000
--- a/common/src/main/kotlin/com/lambda/gui/api/component/core/list/ChildLayer.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.api.component.core.list
-
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.LambdaGui
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.core.IComponent
-import com.lambda.util.Mouse
-import com.lambda.util.math.Rect
-
-open class ChildLayer(
- val gui: LambdaGui,
- val ownerComponent: R,
- private val childRect: () -> Rect,
- private val childAccessible: (T) -> Boolean = { true },
-) : IComponent {
- override val isActive get() = ownerComponent.isActive
- override val childShowAnimation get() = ownerComponent.childShowAnimation
- override val rect get() = childRect()
-
- val children = mutableListOf()
-
- override fun onEvent(e: GuiEvent) {
- children.forEach { child ->
- when (e) {
- is GuiEvent.Tick -> {
- val ownerAccessible = (ownerComponent as? ChildComponent)?.accessible ?: true
- child.accessible =
- childAccessible(child) && child.rect in rect && ownerAccessible && ownerComponent.isActive
- }
-
- is GuiEvent.KeyPress, is GuiEvent.CharTyped, is GuiEvent.MouseScroll -> {
- if (!child.accessible) return@forEach
- }
-
- is GuiEvent.MouseClick -> {
- val newAction = if (child.accessible) e.action else Mouse.Action.Release
- val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse)
- child.onEvent(newEvent)
- return@forEach
- }
- }
-
- child.onEvent(e)
- }
- }
-
- class Drawable(
- gui: LambdaGui,
- owner: R,
- val renderer: RenderLayer,
- contentRect: () -> Rect,
- childAccessible: (T) -> Boolean = { true },
- ) : ChildLayer(gui, owner, contentRect, childAccessible)
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt b/common/src/main/kotlin/com/lambda/gui/component/Alignment.kt
similarity index 70%
rename from common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt
rename to common/src/main/kotlin/com/lambda/gui/component/Alignment.kt
index e4b0754e5..f2376acce 100644
--- a/common/src/main/kotlin/com/lambda/gui/api/component/core/IComponent.kt
+++ b/common/src/main/kotlin/com/lambda/gui/component/Alignment.kt
@@ -15,15 +15,16 @@
* along with this program. If not, see .
*/
-package com.lambda.gui.api.component.core
+package com.lambda.gui.component
-import com.lambda.gui.api.GuiEvent
-import com.lambda.util.math.Rect
-
-interface IComponent {
- val isActive: Boolean get() = true
- val childShowAnimation: Double get() = 1.0
- val rect: Rect
+enum class HAlign(val multiplier: Double, val offset: Double) {
+ LEFT(0.0, -1.0),
+ CENTER(0.5, 0.0),
+ RIGHT(1.0, 1.0)
+}
- fun onEvent(e: GuiEvent)
+enum class VAlign(val multiplier: Double, val offset: Double) {
+ TOP(0.0, -1.0),
+ CENTER(0.5, 0.0),
+ BOTTOM(1.0, 1.0)
}
diff --git a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt b/common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt
similarity index 78%
rename from common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt
rename to common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt
index 3fbb65a28..785f3e826 100644
--- a/common/src/main/kotlin/com/lambda/gui/api/component/core/DockingRect.kt
+++ b/common/src/main/kotlin/com/lambda/gui/component/DockingRect.kt
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.lambda.gui.api.component.core
+package com.lambda.gui.component
import com.lambda.module.modules.client.ClickGui
import com.lambda.util.math.MathUtils.roundToStep
@@ -36,23 +36,21 @@ abstract class DockingRect {
open val allowHAlign = true
open val allowVAlign = true
- open var dockingH = HAlign.LEFT
- set(to) {
- val from = field
- field = to
+ open var dockingH = HAlign.LEFT; set(to) {
+ val from = field
+ field = to
- val delta = to.multiplier - from.multiplier
- relativePos += Vec2d.RIGHT * delta * (size.x - screenSize.x)
- }
+ val delta = to.multiplier - from.multiplier
+ relativePos += Vec2d.RIGHT * delta * (size.x - screenSize.x)
+ }
- open var dockingV = VAlign.TOP
- set(to) {
- val from = field
- field = to
+ open var dockingV = VAlign.TOP; set(to) {
+ val from = field
+ field = to
- val delta = to.multiplier - from.multiplier
- relativePos += Vec2d.BOTTOM * delta * (size.y - screenSize.y)
- }
+ val delta = to.multiplier - from.multiplier
+ relativePos += Vec2d.BOTTOM * delta * (size.y - screenSize.y)
+ }
var screenSize: Vec2d = Vec2d.ZERO
@@ -92,16 +90,4 @@ abstract class DockingRect {
}
} else VAlign.TOP
}
-
- enum class HAlign(val multiplier: Double) {
- LEFT(0.0),
- CENTER(0.5),
- RIGHT(1.0)
- }
-
- enum class VAlign(val multiplier: Double) {
- TOP(0.0),
- CENTER(0.5),
- BOTTOM(1.0)
- }
-}
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt
new file mode 100644
index 000000000..877430948
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/core/FilledRect.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.core
+
+import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.math.Rect
+import java.awt.Color
+
+class FilledRect(
+ owner: Layout
+) : Layout(owner) {
+ @UIRenderPr0p3rty var leftTopRadius = 0.0
+ @UIRenderPr0p3rty var rightTopRadius = 0.0
+ @UIRenderPr0p3rty var rightBottomRadius = 0.0
+ @UIRenderPr0p3rty var leftBottomRadius = 0.0
+
+ @UIRenderPr0p3rty var leftTopColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var rightTopColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var rightBottomColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var leftBottomColor: Color = Color.WHITE
+
+ @UIRenderPr0p3rty var shade = false
+
+ init {
+ properties.interactionPassthrough = true
+
+ onRender {
+ filledRect(
+ rect,
+ leftTopRadius,
+ rightTopRadius,
+ rightBottomRadius,
+ leftBottomRadius,
+ leftTopColor,
+ rightTopColor,
+ rightBottomColor,
+ leftBottomColor,
+ shade
+ )
+ }
+ }
+
+ fun setRadius(radius: Double) {
+ leftTopRadius = radius
+ rightTopRadius = radius
+ rightBottomRadius = radius
+ leftBottomRadius = radius
+ }
+
+ fun setRadius(
+ leftTopRadius: Double,
+ rightTopRadius: Double,
+ rightBottomRadius: Double,
+ leftBottomRadius: Double,
+ ) {
+ this.leftTopRadius = leftTopRadius
+ this.rightTopRadius = rightTopRadius
+ this.rightBottomRadius = rightBottomRadius
+ this.leftBottomRadius = leftBottomRadius
+ }
+
+ fun setColor(color: Color) {
+ leftTopColor = color
+ rightTopColor = color
+ rightBottomColor = color
+ leftBottomColor = color
+ }
+
+ fun setColorH(colorL: Color, colorR: Color) {
+ leftTopColor = colorL
+ rightTopColor = colorR
+ rightBottomColor = colorR
+ leftBottomColor = colorL
+ }
+
+ fun setColorV(colorT: Color, colorB: Color) {
+ leftTopColor = colorT
+ rightTopColor = colorT
+ rightBottomColor = colorB
+ leftBottomColor = colorB
+ }
+
+ companion object {
+ /**
+ * Creates a [FilledRect] component - layout-based rect representation
+ */
+ @UIBuilder
+ fun Layout.rect(
+ block: FilledRect.() -> Unit = {}
+ ) = FilledRect(this).apply(children::add).apply(block)
+
+ /**
+ * Adds a [FilledRect] behind given [layout]
+ */
+ @UIBuilder
+ fun Layout.rectBehind(
+ layout: Layout,
+ block: FilledRect.() -> Unit = {}
+ ) = FilledRect(this).insertLayout(this, layout, false).apply(block)
+
+ /**
+ * Adds a [FilledRect] over given [layout]
+ */
+ @UIBuilder
+ fun Layout.rectOver(
+ layout: Layout,
+ block: FilledRect.() -> Unit = {}
+ ) = FilledRect(this).insertLayout(this, layout, true).apply(block)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt
new file mode 100644
index 000000000..014f0fd1c
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/core/OutlineRect.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.core
+
+import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect
+import com.lambda.gui.component.layout.Layout
+import java.awt.Color
+
+class OutlineRect(
+ owner: Layout
+) : Layout(owner) {
+ @UIRenderPr0p3rty var roundRadius = 0.0
+ @UIRenderPr0p3rty var glowRadius = 1.0
+
+ @UIRenderPr0p3rty var leftTopColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var rightTopColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var rightBottomColor: Color = Color.WHITE
+ @UIRenderPr0p3rty var leftBottomColor: Color = Color.WHITE
+
+ @UIRenderPr0p3rty var shade = false
+
+ init {
+ properties.interactionPassthrough = true
+
+ onRender {
+ outlineRect(
+ rect,
+ roundRadius,
+ glowRadius,
+ leftTopColor,
+ rightTopColor,
+ rightBottomColor,
+ leftBottomColor,
+ shade
+ )
+ }
+ }
+
+ fun setColor(color: Color) {
+ leftTopColor = color
+ rightTopColor = color
+ rightBottomColor = color
+ leftBottomColor = color
+ }
+
+ fun setColorH(colorL: Color, colorR: Color) {
+ leftTopColor = colorL
+ rightTopColor = colorR
+ rightBottomColor = colorR
+ leftBottomColor = colorL
+ }
+
+ fun setColorV(colorT: Color, colorB: Color) {
+ leftTopColor = colorT
+ rightTopColor = colorT
+ rightBottomColor = colorB
+ leftBottomColor = colorB
+ }
+
+ companion object {
+ /**
+ * Creates an [OutlineRect] component - layout-based rect representation
+ */
+ @UIBuilder
+ fun Layout.outline(
+ block: OutlineRect.() -> Unit = {}
+ ) = OutlineRect(this).apply(children::add).apply(block)
+
+ /**
+ * Adds a [OutlineRect] behind given [layout]
+ */
+ @UIBuilder
+ fun Layout.outlineBehind(
+ layout: Layout,
+ block: OutlineRect.() -> Unit = {}
+ ) = OutlineRect(this).insertLayout(this, layout, false).apply(block)
+
+ /**
+ * Creates an [OutlineRect] component - layout-based rect representation
+ */
+ @UIBuilder
+ fun Layout.outlineOver(
+ layout: Layout,
+ block: OutlineRect.() -> Unit = {}
+ ) = OutlineRect(this).insertLayout(this, layout, true).apply(block)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt
new file mode 100644
index 000000000..0f871695a
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/core/TextField.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.core
+
+import com.lambda.graphics.renderer.gui.font.FontRenderer
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.VAlign
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.lerp
+import java.awt.Color
+
+class TextField(
+ owner: Layout,
+) : Layout(owner) {
+ @UIRenderPr0p3rty var text = ""
+ @UIRenderPr0p3rty var color: Color = Color.WHITE
+ @UIRenderPr0p3rty var scale = 1.0
+ @UIRenderPr0p3rty var shadow = true
+
+ @UIRenderPr0p3rty var textHAlignment = HAlign.LEFT
+ @UIRenderPr0p3rty var textVAlignment = VAlign.CENTER
+ @UIRenderPr0p3rty var offsetX = 0.0
+ @UIRenderPr0p3rty var offsetY = 0.0
+
+ val textWidth get() = FontRenderer.getWidth(text, scale)
+ val textHeight get() = FontRenderer.getHeight(scale)
+
+ fun mergeFrom(other: TextField) {
+ text = other.text
+ color = other.color
+ scale = other.scale
+ shadow = other.shadow
+
+ textHAlignment = HAlign.RIGHT
+ textVAlignment = other.textVAlignment
+ offsetX = other.offsetX
+ offsetY = other.offsetY
+ }
+
+ init {
+ properties.interactionPassthrough = true
+
+ onUpdate {
+ position = owner.position
+ size = owner.size
+ }
+
+ onRender {
+ val rx = positionX + lerp(textHAlignment.multiplier, offsetX, width - textWidth - offsetX)
+ val ry = positionY + lerp(textVAlignment.multiplier, offsetY, height - textHeight - offsetY)
+ val renderPos = Vec2d(rx, ry + textHeight * 0.5)
+ drawString(text, renderPos, color, scale, shadow)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a [TextField] component
+ */
+ @UIBuilder
+ fun Layout.textField(
+ block: TextField.() -> Unit = {}
+ ) = TextField(this).apply(children::add).apply(block)
+
+ /**
+ * Adds a [TextField] behind given [layout]
+ */
+ @UIBuilder
+ fun Layout.textFieldBehind(
+ layout: Layout,
+ block: TextField.() -> Unit = {}
+ ) = TextField(this).insertLayout(this, layout, false).apply(block)
+
+ /**
+ * Adds a [TextField] over given [layout]
+ */
+ @UIBuilder
+ fun Layout.textFieldOver(
+ layout: Layout,
+ block: TextField.() -> Unit = {}
+ ) = TextField(this).insertLayout(this, layout, true).apply(block)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt
new file mode 100644
index 000000000..cc61da3d0
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/core/UIBuilder.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.core
+
+import com.lambda.event.events.GuiEvent
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.math.MathUtils.toInt
+
+@DslMarker
+annotation class UIBuilder
+
+@DslMarker
+annotation class LayoutBuilder
+
+@DslMarker
+annotation class UIRenderPr0p3rty
+
+fun T.insertLayout(
+ owner: Layout,
+ base: Layout,
+ next: Boolean
+) = apply {
+ val index = owner.children.indexOf(base)
+ check(index != -1 && base.owner == owner) { "Given layout belongs to different owner" }
+ owner.children.add(index + next.toInt(), this)
+ this.onEvent(GuiEvent.Update)
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt
new file mode 100644
index 000000000..e7b946d1a
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/layout/Layout.kt
@@ -0,0 +1,456 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.layout
+
+import com.lambda.graphics.RenderMain
+import com.lambda.graphics.animation.AnimationTicker
+import com.lambda.event.events.GuiEvent
+import com.lambda.graphics.pipeline.ScissorAdapter
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.VAlign
+import com.lambda.gui.component.core.*
+import com.lambda.util.KeyCode
+import com.lambda.util.Mouse
+import com.lambda.util.math.Rect
+import com.lambda.util.math.Vec2d
+
+/**
+ * Represents a component for creating complex ui structures.
+ */
+open class Layout(
+ val owner: Layout?
+) {
+ var rect
+ get() = Rect.basedOn(position, size)
+ set(value) { position = value.leftTop; size = value.size }
+
+ // ToDo: impl alignmentLayout: Layout, instead of being able to align to the owner only
+ // Position of the component
+ @UIRenderPr0p3rty
+ var position: Vec2d
+ get() = Vec2d(positionX, positionY)
+ set(value) { positionX = value.x; positionY = value.y }
+
+ @UIRenderPr0p3rty
+ var positionX: Double
+ get() = ownerX + (relativePosX + dockingOffsetX).let {
+ if (!properties.clampPosition) return@let it
+ it.coerceAtMost(ownerWidth - width).coerceAtLeast(0.0)
+ }; set(value) { relativePosX = value - ownerX - dockingOffsetX }
+
+ @UIRenderPr0p3rty
+ var positionY: Double
+ get() = ownerY + (relativePosY + dockingOffsetY).let {
+ if (!properties.clampPosition) return@let it
+ it.coerceAtMost(ownerHeight - height).coerceAtLeast(0.0)
+ }; set(value) { relativePosY = value - ownerY - dockingOffsetY }
+
+ val leftTop get() = position
+ val rightTop get() = Vec2d(positionX + width, positionY)
+ val rightBottom get() = Vec2d(positionX + width, positionY + height)
+ val leftBottom get() = Vec2d(positionX, positionY + height)
+
+ private var relativePosX = 0.0
+ private var relativePosY = 0.0
+
+ // Size of the component
+ @UIRenderPr0p3rty
+ var size: Vec2d
+ get() = Vec2d(width, height)
+ set(value) { width = value.x; height = value.y }
+
+ @UIRenderPr0p3rty var width = 0.0
+ @UIRenderPr0p3rty var height = 0.0
+
+ // Horizontal alignment
+ var horizontalAlignment = HAlign.LEFT; set(to) {
+ val from = field
+ field = to
+
+ val delta = to.multiplier - from.multiplier
+ relativePosX += delta * (width - ownerWidth)
+ }
+
+ private val dockingOffsetX get() = if (horizontalAlignment == HAlign.LEFT) 0.0
+ else (ownerWidth - width) * horizontalAlignment.multiplier
+
+ // Vertical alignment
+ var verticalAlignment = VAlign.TOP; set(to) {
+ val from = field
+ field = to
+
+ val delta = to.multiplier - from.multiplier
+ relativePosY += delta * (height - ownerHeight)
+ }
+
+ private val dockingOffsetY get() = if (verticalAlignment == VAlign.TOP) 0.0
+ else (ownerHeight - height) * verticalAlignment.multiplier
+
+ // Use screen limits if [owner] is null
+ private var screenSize = Vec2d.ZERO
+
+ // Owner params (cached, due to the nullability of [owner])
+ private var ownerX = 0.0
+ private var ownerY = 0.0
+ private var ownerWidth = 0.0
+ private var ownerHeight = 0.0
+
+ /**
+ * Configurable properties of the component
+ */
+ val properties = LayoutProperties()
+
+ // Structure
+ val children = mutableListOf()
+ var selectedChild: Layout? = null
+ protected open val renderSelf: Boolean get() = width > 1 && height > 1
+ protected open val scissorRect get() = rect
+
+ // Inputs
+ protected var mousePosition = Vec2d.ZERO; set(value) {
+ if (field == value) return
+ field = value
+
+ selectedChild = if (isHovered) children.lastOrNull {
+ if (it.properties.interactionPassthrough) return@lastOrNull false
+ val xh = (value.x - it.positionX) in 0.0..it.width
+ val yh = (value.y - it.positionY) in 0.0..it.height
+ xh && yh
+ } else null
+ }
+ open val isHovered get() = owner?.let { it.selectedChild == this } ?: true
+
+ var pressedButton: Mouse.Button? = null
+ protected val isPressed get() = pressedButton != null
+
+ // Actions
+ private val showActions = mutableListOf Unit>()
+ private val hideActions = mutableListOf Unit>()
+ private val tickActions = mutableListOf Unit>()
+ private val updateActions = mutableListOf Unit>()
+ private val renderActions = mutableListOf Unit>()
+ private val keyPressActions = mutableListOf Unit>()
+ private val charTypedActions = mutableListOf Unit>()
+ private val mouseClickActions = mutableListOf Unit>()
+ private val mouseActions = mutableListOf Unit>()
+ private val mouseMoveActions = mutableListOf Unit>()
+ private val mouseScrollActions = mutableListOf Unit>()
+
+ /**
+ * Performs the action on this layout
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.use(action: T.() -> Unit) {
+ action(this)
+ }
+
+ /**
+ * Sets the action to be performed when the element gets shown.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onShow(action: T.() -> Unit) {
+ showActions += { action() }
+ }
+
+ /**
+ * Sets the action to be performed when the element gets hidden.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onHide(action: T.() -> Unit) {
+ hideActions += { action() }
+ }
+
+ /**
+ * Sets the action to be performed on each tick.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onTick(action: T.() -> Unit) {
+ tickActions += { action() }
+ }
+
+ /**
+ * Sets the update action to be performed before each frame.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onUpdate(action: T.() -> Unit) {
+ action(this)
+ updateActions += { action() }
+ }
+
+ /**
+ * Sets the action to be performed on each frame.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onRender(action: T.() -> Unit) {
+ renderActions += { action() }
+ }
+
+ /**
+ * Sets the action to be performed when a key gets pressed.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onKeyPress(action: T.(key: KeyCode) -> Unit) {
+ keyPressActions += { key -> action(key) }
+ }
+
+ /**
+ * Sets the action to be performed when user types a char.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onCharTyped(action: T.(char: Char) -> Unit) {
+ charTypedActions += { char -> action(char) }
+ }
+
+ /**
+ * Sets the action to be performed when mouse button gets clicked.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onMouse(button: Mouse.Button? = null, action: Mouse.Action? = null, block: T.(Mouse.Button) -> Unit) {
+ mouseClickActions += { butt, act ->
+ if ((butt == button || button == null) && (act == action || action == null)) block(butt)
+ }
+ }
+
+ /**
+ * Sets the action to be performed when mouse button gets released and this layout was clicked.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onMouseAction(button: Mouse.Button? = null, acceptNotHovered: Boolean = false, action: T.(Mouse.Button) -> Unit) {
+ mouseActions += {
+ if (it == button || button == null && (isHovered || !acceptNotHovered)) action(it)
+ }
+ }
+
+ /**
+ * Sets the action to be performed when mouse moves.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onMouseMove(action: T.(mouse: Vec2d) -> Unit) {
+ mouseMoveActions += { mouse -> action(mouse) }
+ }
+
+ /**
+ * Sets the action to be performed on mouse scroll.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onMouseScroll(action: T.(delta: Double) -> Unit) {
+ mouseScrollActions += { delta -> action(delta) }
+ }
+
+ /**
+ * Removes this layout from its parent
+ */
+ fun destroy() {
+ check(owner != null) {
+ "Unable to destroy root layout. Owner is null."
+ }
+
+ check(owner.children.remove(this)) {
+ "destroy() called twice. The layout was already removed"
+ }
+ }
+
+ init {
+ onUpdate { // Update the layout
+ screenSize = RenderMain.screenSize
+
+ // Update relative position and bounds
+ ownerX = owner?.positionX ?: ownerX
+ ownerY = owner?.positionY ?: ownerY
+ ownerWidth = owner?.width ?: screenSize.x
+ ownerHeight = owner?.height ?: screenSize.y
+ }
+ }
+
+ fun onEvent(e: GuiEvent) {
+ // Update self
+ when (e) {
+ is GuiEvent.Show -> {
+ pressedButton = null
+ selectedChild = null
+ mousePosition = Vec2d.ONE * -1000.0
+ showActions.forEach { it(this) }
+ }
+ is GuiEvent.Hide -> {
+ hideActions.forEach { it(this) }
+ }
+ is GuiEvent.Tick -> {
+ tickActions.forEach { it(this) }
+ }
+ is GuiEvent.KeyPress -> {
+ keyPressActions.forEach { it(this, e.key) }
+ }
+ is GuiEvent.CharTyped -> {
+ charTypedActions.forEach { it(this, e.char) }
+ }
+ is GuiEvent.Update -> {
+ updateActions.forEach { it(this) }
+ }
+ is GuiEvent.Render -> {
+ if (!renderSelf) return
+ }
+ is GuiEvent.MouseMove -> {
+ mousePosition = e.mouse
+
+ mouseMoveActions.forEach { it(this, e.mouse) }
+ }
+ is GuiEvent.MouseScroll -> {
+ mousePosition = e.mouse
+
+ if (!isHovered) return
+ mouseScrollActions.forEach { it(this, e.delta) }
+ }
+ is GuiEvent.MouseClick -> {
+ mousePosition = e.mouse
+
+ val action = if (isHovered) e.action else Mouse.Action.Release
+
+ val prevPressed = pressedButton
+ pressedButton = e.button.takeIf { action == Mouse.Action.Click }
+
+ if (pressedButton == null) prevPressed?.let { button ->
+ mouseActions.forEach {
+ it.invoke(this, button)
+ }
+ }
+
+ mouseClickActions.forEach { it(this, e.button, action) }
+ }
+ }
+
+ // Update children
+ children.forEach { child ->
+ if (e is GuiEvent.Render) return@forEach
+ if (e is GuiEvent.MouseClick) {
+ val newAction = if (child.isHovered) e.action else Mouse.Action.Release
+
+ val newEvent = GuiEvent.MouseClick(e.button, newAction, e.mouse)
+ child.onEvent(newEvent)
+ return@forEach
+ }
+
+ child.onEvent(e)
+ }
+
+ if (e is GuiEvent.Render) {
+ val block = {
+ renderActions.forEach { it(this) }
+ if (renderSelf) children.forEach { it.onEvent(e) }
+ }
+
+ if (!properties.scissor) block()
+ else ScissorAdapter.scissor(scissorRect, block)
+ }
+ }
+
+ companion object {
+ /**
+ * Creates an empty [Layout].
+ *
+ * @param block Actions to perform within this component.
+ *
+ * Check [Layout] description for more info about batching.
+ */
+ @UIBuilder
+ fun Layout.layout(
+ block: Layout.() -> Unit = {},
+ ) = Layout(this)
+ .apply(children::add).apply(block)
+
+ /**
+ * Creates an empty [Layout] behind given [layout]
+ *
+ * @param block Actions to perform within this component.
+ *
+ * Check [Layout] description for more info about batching.
+ */
+ @UIBuilder
+ fun Layout.layoutBehind(
+ layout: Layout,
+ block: Layout.() -> Unit = {},
+ ) = Layout(this)
+ .insertLayout(this, layout, false).apply(block)
+
+ /**
+ * Creates an empty [Layout] over given [layout].
+ *
+ * @param block Actions to perform within this component.
+ *
+ * Check [Layout] description for more info about batching.
+ */
+ @UIBuilder
+ fun Layout.layoutOver(
+ layout: Layout,
+ block: Layout.() -> Unit = {},
+ ) = Layout(this)
+ .insertLayout(this, layout, true).apply(block)
+
+ /**
+ * Creates new [AnimationTicker].
+ *
+ * Use it to create and manage animations.
+ *
+ * It's ok to have multiple tickers per component if you need to tick different animations at different timings.
+ *
+ * @param register Whether to tick this [AnimationTicker].
+ * Otherwise, you will have to tick it manually
+ */
+ @UIBuilder
+ fun Layout.animationTicker(register: Boolean = true) = AnimationTicker().apply {
+ if (register) onTick {
+ this@apply.tick()
+ }
+ }
+
+ /**
+ * Creates new [Mouse.CursorController].
+ *
+ * Use it to set the mouse cursor type for various conditions: hovering, resizing, typing etc...
+ */
+ @UIBuilder
+ fun Layout.cursorController(): Mouse.CursorController {
+ val con = Mouse.CursorController()
+ onHide { con.reset() }
+ return con
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt b/common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt
similarity index 61%
rename from common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt
rename to common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt
index 3d32800ea..a8c02726c 100644
--- a/common/src/main/kotlin/com/lambda/gui/GuiConfigurable.kt
+++ b/common/src/main/kotlin/com/lambda/gui/component/layout/LayoutProperties.kt
@@ -15,16 +15,21 @@
* along with this program. If not, see .
*/
-package com.lambda.gui
+package com.lambda.gui.component.layout
-import com.lambda.gui.impl.clickgui.LambdaClickGui
-import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow
-import com.lambda.module.tag.ModuleTag
+class LayoutProperties {
+ /**
+ * If true, interactions pass through to elements beneath this one.
+ */
+ var interactionPassthrough = false
-object GuiConfigurable : AbstractGuiConfigurable(
- LambdaClickGui, ModuleTag.defaults, "gui"
-) {
- var customWindows by setting("custom windows", listOf())
+ /**
+ * If true, this element's rectangle is clamped within parent's bounds.
+ */
+ var clampPosition = false
- override fun load() = "Loaded GUI Configurable"
+ /**
+ * If true, anything drawn onto this render layer are clipped within this rect.
+ */
+ var scissor = false
}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt
new file mode 100644
index 000000000..f736c0c0f
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/window/TitleBar.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.window
+
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.component.core.TextField.Companion.textField
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.Mouse
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.transform
+
+/**
+ * Represents a titlebar component
+ */
+class TitleBar(
+ owner: Window,
+ title: String,
+ drag: Boolean
+) : Layout(owner) {
+ private var dragOffset: Vec2d? = null
+
+ init {
+ onShow {
+ dragOffset = null
+ }
+
+ onUpdate {
+ width = owner.width
+ height = ClickGui.titleBarHeight
+ }
+
+ onMouse { dragOffset = null }
+ onMouse(Mouse.Button.Left, Mouse.Action.Click) {
+ if (drag) dragOffset = mousePosition - owner.position
+ }
+
+ onMouseMove { mouse ->
+ dragOffset?.let { drag ->
+ owner.position = mouse - drag
+ }
+ }
+ }
+
+ val backgroundRect = rect {
+ onUpdate {
+ position = this@TitleBar.position
+ size = this@TitleBar.size
+ setColor(ClickGui.titleBackgroundColor)
+
+ val radius = ClickGui.roundRadius
+ leftTopRadius = radius
+ rightTopRadius = radius
+
+ val bottomRadius = transform(
+ owner.height,
+ height,
+ height + 1,
+ radius,
+ 0.0
+ )
+ leftBottomRadius = bottomRadius
+ rightBottomRadius = bottomRadius
+
+ shade = ClickGui.backgroundShade
+ }
+ }
+
+ val textField = textField {
+ text = title
+ textHAlignment = HAlign.CENTER
+
+ onUpdate {
+ offsetX = ClickGui.fontOffset
+ scale = ClickGui.fontScale
+ }
+ }
+
+ companion object {
+ @UIBuilder
+ fun Window.titleBar(
+ text: String,
+ drag: Boolean
+ ) = TitleBar(this, text, drag).apply(children::add)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt
new file mode 100644
index 000000000..0843e6bed
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/window/Window.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.window
+
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.ScreenLayout
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.component.core.LayoutBuilder
+import com.lambda.gui.component.core.OutlineRect.Companion.outline
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.window.TitleBar.Companion.titleBar
+import com.lambda.gui.component.window.WindowContent.Companion.windowContent
+import com.lambda.gui.impl.clickgui.core.AnimatedChild
+import com.lambda.util.Mouse
+import com.lambda.util.math.MathUtils.toInt
+import com.lambda.util.math.Rect
+import com.lambda.util.math.Vec2d
+
+/**
+ * Represents a window component
+ *
+ * Consists of titlebar and content layout
+ */
+open class Window(
+ owner: Layout,
+ initialTitle: String = "Untitled",
+ initialPosition: Vec2d = Vec2d.ZERO,
+ initialSize: Vec2d = Vec2d(110, 350),
+ draggable: Boolean = true,
+ scrollable: Boolean = true,
+ private val minimizing: Minimizing = Minimizing.Relative,
+ private val resizable: Boolean = true,
+ val autoResize: AutoResize = AutoResize.Disabled
+) : Layout(owner) {
+ protected val animation = animationTicker()
+ private val cursorController = cursorController()
+
+ val titleBar = titleBar(initialTitle, draggable)
+
+ val titleBarBackground by titleBar::backgroundRect
+ val contentBackground = rect {
+ onUpdate {
+ rect = Rect(titleBar.leftBottom, this@Window.rightBottom)
+ setColor(ClickGui.backgroundColor)
+
+ leftBottomRadius = ClickGui.roundRadius
+ rightBottomRadius = ClickGui.roundRadius
+
+ shade = ClickGui.backgroundShade
+ }
+ }
+
+ val content = windowContent(scrollable)
+
+ val outlineRect = outline {
+ onUpdate {
+ position = this@Window.position
+ size = this@Window.size
+
+ setColor(ClickGui.outlineColor)
+
+ roundRadius = ClickGui.roundRadius
+ glowRadius = ClickGui.outlineWidth * ClickGui.outline.toInt().toDouble()
+
+ shade = ClickGui.outlineShade
+ }
+ }
+
+ // Actions
+ private val expandActions = mutableListOf Unit>()
+ private val minimizeActions = mutableListOf Unit>()
+
+ /**
+ * Sets the action to be performed when the window content gets opened.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onWindowExpand(action: T.() -> Unit) {
+ expandActions += { action() }
+ }
+
+ /**
+ * Sets the action to be performed when the window content gets closed.
+ *
+ * @param action The action to be performed.
+ */
+ @LayoutBuilder
+ fun T.onWindowMinimize(action: T.() -> Unit) {
+ minimizeActions += { action() }
+ }
+
+ // Position
+ // ToDo find a way to animate this only when dragging
+ /*private val renderX by animation.exp(position::x, 0.8)
+ private val renderY by animation.exp(position::y, 0.8)
+ private val renderPosition get() = Vec2d(renderX, renderY)*/
+
+ // Minimizing
+ var isMinimized = false; set(value) {
+ if (field == value) return
+ field = value
+
+ val actions = if (!value) expandActions else minimizeActions
+ actions.forEach { it(this) }
+ }
+
+ var isExpand
+ get() = !isMinimized
+ set(value) { isMinimized = !value }
+
+ var windowWidth = initialSize.x
+ var windowHeight = initialSize.y
+
+ var widthAnimation by animation.exp(0.8, ::windowWidth)
+ var heightAnimation by animation.exp(
+ min = { 0.0 },
+ max = { if (minimizing == Minimizing.Relative) targetHeight else 1.0 },
+ speed = 0.7,
+ flag = { !isMinimized }
+ )
+
+ val targetHeight get() = if (!autoResize.enabled) windowHeight - titleBar.height else content.height
+
+ // Resizing
+ private var resizeX: Double? = null
+ private var resizeY: Double? = null
+ private var resizeXHovered = false
+ private var resizeYHovered = false
+
+ init {
+ position = initialPosition
+ properties.clampPosition = owner is ScreenLayout
+
+ onUpdate {
+ width = widthAnimation
+ height = titleBar.height + when (minimizing) {
+ Minimizing.Disabled -> targetHeight
+ Minimizing.Relative -> heightAnimation
+ Minimizing.Absolute -> heightAnimation * targetHeight
+ }
+ }
+
+ titleBar.onMouseAction(Mouse.Button.Right) {
+ // Toggle minimizing state when right-clicking title bar
+ if (minimizing == Minimizing.Disabled) return@onMouseAction
+ isMinimized = !isMinimized
+ }
+
+ content.onUpdate {
+ val animatedChildren = content.children
+ .filterIsInstance()
+ .filter { it.isShown }
+
+ animatedChildren.forEachIndexed { i, it ->
+ it.index = i
+ it.lastIndex = animatedChildren.lastIndex
+ }
+ }
+
+ onShow {
+ resizeX = null
+ resizeY = null
+ resizeXHovered = false
+ resizeYHovered = false
+
+ heightAnimation = 0.0
+ /*heightAnimation = when {
+ isMinimized -> 0.0
+ minimizing == Minimizing.Relative -> targetHeight
+ else -> 1.0
+ }*/
+ }
+
+ onTick {
+ // Update cursor
+ val rxh = resizeXHovered || resizeX != null
+ val ryh = resizeYHovered || resizeY != null
+
+ val cursor = when {
+ rxh && ryh -> Mouse.Cursor.ResizeHV
+ rxh -> Mouse.Cursor.ResizeH
+ ryh -> Mouse.Cursor.ResizeV
+ else -> Mouse.Cursor.Arrow
+ }
+
+ cursorController.setCursor(cursor)
+ }
+
+ // Update resize dragging offsets
+ onMouse { resizeX = null; resizeY = null }
+ onMouse(Mouse.Button.Left, Mouse.Action.Click) {
+ if (resizeXHovered) resizeX = mousePosition.x - width
+ if (resizeYHovered) resizeY = mousePosition.y - height
+ }
+
+ onMouseMove {
+ resizeXHovered = false
+ resizeYHovered = false
+
+ if (!resizable || isMinimized) return@onMouseMove
+
+ // Hover state update
+ if (selectedChild != titleBar && content.selectedChild == null && isHovered) {
+ resizeXHovered = mousePosition in Rect(
+ rightTop - Vec2d(RESIZE_RANGE, 0.0),
+ rightBottom
+ )
+
+ resizeYHovered = !autoResize.enabled && mousePosition in Rect(
+ leftBottom - Vec2d(0.0, RESIZE_RANGE),
+ rightBottom
+ )
+ }
+
+ // Resize
+ if (resizeX != null || resizeY != null) {
+ resizeX?.let { rx ->
+ windowWidth = (mousePosition.x - rx).coerceIn(80.0, 1000.0)
+ }
+
+ resizeY?.let { ry ->
+ windowHeight = (mousePosition.y - ry).coerceIn(titleBar.height + RESIZE_RANGE, 1000.0)
+ }
+ }
+ }
+ }
+
+ enum class AutoResize(private val isEnabled: () -> Boolean) {
+ Disabled({ false }),
+ ByConfig({ ClickGui.autoResize }),
+ ForceEnabled({ true });
+
+ val enabled get() = isEnabled()
+ }
+
+ /**
+ * [Disabled] -> No ability to minimize the window
+ * [Relative] -> Animation follows the height of the component ( animation(0.0, height) ) (height change is animated)
+ * [Absolute] -> Animation does not depend on the height ( animation(0.0, 1.0) * height ) (height change instantly affects the height)
+ */
+ enum class Minimizing {
+ Disabled,
+ Relative,
+ Absolute;
+ }
+
+ companion object {
+ /**
+ * Creates new empty [Window]
+ *
+ * @param position The initial position of the window
+ *
+ * @param size The initial size of the window
+ *
+ * @param title The title of the window
+ *
+ * @param draggable Whether to allow user to drag the window
+ *
+ * @param scrollable Whether to let user scroll the content
+ * This will also make your elements be vertically ordered
+ *
+ * @param minimizing The [Minimizing] mode.
+ *
+ * @param resizable Whether to allow user to resize the window
+ *
+ * @param autoResize Indicates if this window could be automatically resized based on content height
+ *
+ * @param block Actions to perform within content space of the window
+ */
+ @UIBuilder
+ fun Layout.window(
+ position: Vec2d = Vec2d.ZERO,
+ size: Vec2d = Vec2d(120.0, 300.0),
+ title: String = "Untitled",
+ draggable: Boolean = true,
+ scrollable: Boolean = true,
+ minimizing: Minimizing = Minimizing.Relative,
+ resizable: Boolean = true,
+ autoResize: AutoResize = AutoResize.Disabled,
+ block: WindowContent.() -> Unit = {}
+ ) = Window(
+ this, title,
+ position, size,
+ draggable, scrollable, minimizing, resizable,
+ autoResize
+ ).apply(children::add).apply {
+ block(this.content)
+ }
+
+ private const val RESIZE_RANGE = 5.0
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt
new file mode 100644
index 000000000..9a9d2cc5c
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/component/window/WindowContent.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.component.window
+
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.gui.component.core.LayoutBuilder
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.core.AnimatedChild
+import com.lambda.util.math.MathUtils.toInt
+import com.lambda.util.math.Rect
+import kotlin.math.abs
+
+class WindowContent(
+ owner: Window,
+ scrollable: Boolean
+) : Layout(owner) {
+ private val window = owner
+ private val animation = animationTicker(false)
+
+ private var dwheel = 0.0
+ private var scrollOffset = 0.0
+ private var rubberbandDelta = 0.0
+
+ private var renderScrollOffset by animation.exp(0.7) { scrollOffset + rubberbandDelta }
+
+ override val scissorRect: Rect
+ get() = Rect(window.titleBar.leftBottom, window.rightBottom)
+
+ override val renderSelf: Boolean
+ get() = window.heightAnimation > 0.05
+
+ /**
+ * Orders the children set vertically
+ */
+ @LayoutBuilder
+ fun listify() {
+ children.forEachIndexed { i, it ->
+ val prev = children.getOrNull(i - 1) ?: run {
+ it.onUpdate {
+ positionY = this@WindowContent.positionY + ClickGui.padding
+ }
+
+ return@forEachIndexed
+ }
+
+ it.onUpdate {
+ positionY = prev.positionY + layoutHeight(prev, true)
+ }
+ }
+ }
+
+ init {
+ properties.scissor = true
+
+ onUpdate {
+ positionX = owner.titleBar.positionX
+ positionY = owner.titleBar.let { it.positionY + it.height } + renderScrollOffset * scrollable.toInt()
+ width = owner.width
+
+ height = ClickGui.padding * 2
+
+ val lastIndex = children.lastIndex
+ children.forEachIndexed { i, it ->
+ height += layoutHeight(it, false, i == lastIndex)
+ }
+ }
+
+ onShow {
+ dwheel = 0.0
+ scrollOffset = 0.0
+ rubberbandDelta = 0.0
+ renderScrollOffset = 0.0
+ }
+
+ onTick {
+ scrollOffset = if (!owner.autoResize.enabled) {
+ scrollOffset + dwheel
+ } else 0.0
+
+ dwheel = 0.0
+
+ val prevOffset = scrollOffset
+ scrollOffset = scrollOffset.coerceAtLeast(
+ owner.targetHeight - height
+ ).coerceAtMost(0.0)
+
+ rubberbandDelta += prevOffset - scrollOffset
+ rubberbandDelta *= 0.5
+ if (abs(rubberbandDelta) < 0.05) rubberbandDelta = 0.0
+
+ animation.tick()
+ }
+
+ onMouseScroll { delta ->
+ dwheel += delta * 10.0
+ }
+ }
+
+ private fun layoutHeight(layout: Layout, animate: Boolean, isLast: Boolean = false): Double {
+ var height = layout.height + ClickGui.listStep * (!isLast).toInt()
+ val animated = layout as? AnimatedChild ?: return height
+
+ height *= if (!animate) animated.staticShowAnimation
+ else animated.showAnimation
+
+ return height
+ }
+
+ companion object {
+ /**
+ * Creates an empty [WindowContent] component
+ */
+ @UIBuilder
+ fun Window.windowContent(scrollable: Boolean) =
+ WindowContent(this, scrollable).apply(children::add)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt
deleted file mode 100644
index 73e083113..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/AbstractClickGui.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl
-
-import com.lambda.Lambda.mc
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.graphics.buffer.FrameBuffer
-import com.lambda.graphics.shader.Shader
-import com.lambda.gui.AbstractGuiConfigurable
-import com.lambda.gui.GuiConfigurable
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.LambdaGui
-import com.lambda.gui.api.component.WindowComponent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.gui.impl.clickgui.windows.ModuleWindow
-import com.lambda.gui.impl.clickgui.windows.tag.CustomModuleWindow
-import com.lambda.gui.impl.clickgui.windows.tag.TagWindow
-import com.lambda.module.Module
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.util.Mouse
-import kotlin.reflect.KMutableProperty0
-
-abstract class AbstractClickGui(name: String, owner: Module? = null) : LambdaGui(name, owner) {
- protected var hoveredWindow: WindowComponent<*>? = null
- protected var closing = false
-
- final override var childShowAnimation by animation.exp(
- 0.0, 1.0,
- { if (closing) ClickGui.closeSpeed else ClickGui.openSpeed }
- ) { !closing }; private set
-
- val windows = ChildLayer, AbstractClickGui>(this, this, ::rect) { child ->
- child == hoveredWindow && !closing
- }
-
- private val frameBuffer = FrameBuffer()
- private val shader = Shader("post/cgui_animation", "renderer/pos_tex")
-
- abstract val moduleFilter: (Module) -> Boolean
- abstract val configurable: AbstractGuiConfigurable
-
- private var lastTickedUpdate = 0L
-
- private val actionPool = ArrayDeque<() -> Unit>()
- fun scheduleAction(block: () -> Unit) = actionPool.add(block)
-
- override fun onEvent(e: GuiEvent) {
- while (actionPool.isNotEmpty()) actionPool.removeLast().invoke()
-
- when (e) {
- is GuiEvent.Render -> {
- if (childShowAnimation < 0.99) {
- frameBuffer.write {
- windows.onEvent(e)
- }.read(shader) {
- it["u_Progress"] = childShowAnimation
- }
-
- return
- }
- }
-
- is GuiEvent.Show -> {
- hoveredWindow = null
- closing = false
- childShowAnimation = 0.0
- updateWindows()
- }
-
- is GuiEvent.Tick -> {
- val time = System.currentTimeMillis()
- if (time - lastTickedUpdate > 1000L) {
- lastTickedUpdate = time
- updateWindows()
- }
-
- if (closing && childShowAnimation < 0.01) mc.setScreen(null)
- }
-
- is GuiEvent.MouseClick -> {
- if (e.action == Mouse.Action.Click) hoveredWindow?.focus()
- }
-
- is GuiEvent.MouseMove -> {
- hoveredWindow = windows.children.lastOrNull { child ->
- e.mouse in child.rect
- }
- }
- }
-
- windows.onEvent(e)
- }
-
- fun unfocusSettings() {
- windows.children.filterIsInstance().forEach { moduleWindow ->
- moduleWindow.contentComponents.children.forEach { moduleButton ->
- moduleButton.settingsLayer.children.forEach(SettingButton<*, *>::unfocus)
- }
- }
- }
-
- private inline fun syncWindows(prop: KMutableProperty0>) = windows.apply {
- var configWindows by prop
-
- // Add windows from config
- configWindows.filter { it !in children }.forEach(children::add)
-
- // Remove outdated/deleted windows
- children.removeIf {
- it is T && it !in configWindows
- }
-
- // Update config
- configWindows = children.filterIsInstance().toMutableList()
- }
-
- private fun updateWindows() {
- syncWindows(configurable::mainWindows)
-
- (configurable as? GuiConfigurable)?.let {
- syncWindows(it::customWindows)
- }
- }
-
- override fun close() {
- if (!isOpen) return
- closing = true
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt
deleted file mode 100644
index 018b31660..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/LambdaClickGui.kt
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui
-
-import com.lambda.gui.GuiConfigurable
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.button.ButtonComponent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.hudgui.LambdaHudGui
-import com.lambda.module.HudModule
-import com.lambda.module.Module
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.module.modules.client.GuiSettings.primaryColor
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.multAlpha
-import java.awt.Color
-
-object LambdaClickGui : AbstractClickGui("ClickGui", ClickGui) {
- override val moduleFilter: (Module) -> Boolean = {
- it !is HudModule
- }
-
- override val configurable get() = GuiConfigurable
-
- private val buttonRenderer = RenderLayer()
- private val buttons = ChildLayer.Drawable(this, this, buttonRenderer, ::rect) {
- hoveredWindow == null && !closing
- }
-
- override fun onEvent(e: GuiEvent) {
- buttons.onEvent(e)
- if (e is GuiEvent.Render) buttonRenderer.render()
-
- super.onEvent(e)
- }
-
- init {
- buttons.children.add(object : ButtonComponent(buttons) {
- override val position: Vec2d get() = screenSize - size - Vec2d.ONE * 5.0
- override val size = Vec2d(30.0, 15.0)
- override val text = "HUD"
- override val centerText = true
- override val roundRadius = ClickGui.windowRadius
-
- override var activeAnimation; get() = pressAnimation; set(_) {}
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- if (e is GuiEvent.Render) {
- val rect = rect.shrink(interactAnimation)
-
- // Background
- renderer.filled.build(
- rect = rect,
- roundRadius = ClickGui.windowRadius,
- color = GuiSettings.backgroundColor.multAlpha(childShowAnimation),
- shade = GuiSettings.shadeBackground
- )
-
- // Outline
- renderer.outline.build(
- rect = rect,
- roundRadius = ClickGui.windowRadius,
- glowRadius = ClickGui.glowRadius,
- color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(
- childShowAnimation
- ),
- shade = GuiSettings.shadeBackground
- )
- }
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- LambdaHudGui.show()
- }
- })
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt
new file mode 100644
index 000000000..8d204562c
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/ModuleWindow.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui
+
+import com.lambda.module.tag.ModuleTag
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.component.window.Window
+import com.lambda.gui.component.window.WindowContent
+import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.backgroundTint
+import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.moduleLayout
+import com.lambda.module.ModuleRegistry
+import com.lambda.util.math.Vec2d
+
+class ModuleWindow(
+ owner: Layout,
+ val tag: ModuleTag, // todo: tag system
+ initialPosition: Vec2d
+) : Window(owner, tag.name, initialPosition, minimizing = Minimizing.Absolute, autoResize = AutoResize.ByConfig) {
+ init {
+ backgroundTint()
+
+ val modules = ModuleRegistry.modules
+ .filter { it.defaultTags.firstOrNull() == tag }
+ .map { module -> content.moduleLayout(module) }
+
+ content.listify()
+
+ val minimize = {
+ modules.forEach {
+ it.isMinimized = true
+ }
+ }
+
+ onWindowExpand { minimize() }
+ onWindowMinimize { minimize() }
+ }
+
+ companion object {
+ /**
+ * Creates a [ModuleWindow]
+ */
+ @UIBuilder
+ fun Layout.moduleWindow(
+ tag: ModuleTag,
+ position: Vec2d = Vec2d.ZERO,
+ block: WindowContent.() -> Unit = {}
+ ) = ModuleWindow(this, tag, position).apply(children::add).apply {
+ block(this.content)
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt
deleted file mode 100644
index 33f287476..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/ModuleButton.kt
+++ /dev/null
@@ -1,221 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons
-
-import com.lambda.config.settings.NumericSetting
-import com.lambda.config.settings.StringSetting
-import com.lambda.config.settings.comparable.BooleanSetting
-import com.lambda.config.settings.comparable.EnumSetting
-import com.lambda.config.settings.complex.KeyBindSetting
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.graphics.gl.Scissor.scissor
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.WindowComponent
-import com.lambda.gui.api.component.button.ListButton
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.clickgui.buttons.setting.*
-import com.lambda.module.Module
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.sound.LambdaSound
-import com.lambda.sound.SoundManager.playSoundRandomly
-import com.lambda.util.Mouse
-import com.lambda.util.math.MathUtils.toInt
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.setAlpha
-import com.lambda.util.math.transform
-import java.awt.Color
-import kotlin.math.abs
-
-class ModuleButton(
- val module: Module,
- override val owner: ChildLayer.Drawable>,
-) : ListButton(owner) {
- override val text get() = module.name
- private val enabled get() = module.isEnabled
-
- override var activeAnimation by animation.exp(0.0, 1.0, 0.15, ::enabled)
- private val toggleFxDirection by animation.exp(0.0, 1.0, 0.7, ::enabled)
-
- override val listStep: Double get() = super.listStep + renderHeight
-
- private var isOpen = false
- override val isActive get() = isOpen
-
- private val openAnimation by animation.exp(0.0, 1.0, 0.7, ::isOpen)
- override val childShowAnimation get() = lerp(owner.childShowAnimation, 0.0, openAnimation)
-
- private var settingsHeight = 0.0
- private var renderHeight by animation.exp(::settingsHeight, 0.6)
- private val settingsRect
- get() = rect
- .moveFirst(Vec2d(0.0, size.y + super.listStep))
- .moveSecond(Vec2d(0.0, renderHeight))
-
- private val settingsRenderer = RenderLayer()
- val settingsLayer =
- ChildLayer.Drawable, ModuleButton>(owner.gui, this, settingsRenderer, ::settingsRect) {
- it.visible && abs(settingsHeight - renderHeight) < 3
- }
-
- init {
- // TODO: resort when all settings are implemented
- module.settings.mapNotNull {
- when (it) {
- is BooleanSetting -> BooleanButton(it, settingsLayer)
- is NumericSetting<*> -> NumberSlider(it, settingsLayer)
- is StringSetting -> StringButton(it, settingsLayer)
- is EnumSetting<*> -> EnumSlider(it, settingsLayer)
- is KeyBindSetting -> BindButton(it, settingsLayer)
- else -> null
- }
- }.forEach(settingsLayer.children::add)
- }
-
- override fun onEvent(e: GuiEvent) {
- when (e) {
- is GuiEvent.Show -> {
- isOpen = false
- updateHeight()
- renderHeight = settingsHeight
- }
-
- is GuiEvent.Tick -> {
- if (renderHeight < 0.5) return
- updateHeight()
-
- var y = 0.0
- settingsLayer.children.filter(SettingButton<*, *>::visible).forEach { button ->
- button.heightOffset = y
- y += button.size.y + button.listStep
- }
- }
-
- is GuiEvent.Render -> {
- super.onEvent(e)
-
- settingsRenderer.font.scaleMultiplier = ClickGui.settingsFontScale
-
- // Shadow
- renderer.filled.apply {
- val rect = Rect(
- rect.leftTop + Vec2d(0.0, size.y),
- rect.rightTop + Vec2d(0.0, size.y + 5.0)
- )
-
- val progress = transform(renderHeight, 0.0, 10.0, 0.0, 1.0).coerceIn(0.0, 1.0)
- val topColor = Color.BLACK.setAlpha(0.2 * progress * showAnimation)
- val bottomColor = Color.BLACK.setAlpha(0.0)
-
- build(rect, 0.0, topColor, topColor, bottomColor, bottomColor)
- }
-
- // Bottom shadow
- renderer.filled.apply {
- val last = this@ModuleButton.owner.ownerComponent.contentComponents.children.lastOrNull()
- val show = this@ModuleButton != last
-
- val rect = Rect(
- settingsRect.leftBottom - Vec2d(0.0, 5.0),
- settingsRect.rightBottom
- )
-
- val progress = transform(renderHeight, 0.0, 10.0, 0.0, 1.0).coerceIn(0.0, 1.0) * show.toInt()
- val topColor = Color.BLACK.setAlpha(0.0)
- val bottomColor = Color.BLACK.setAlpha(0.2 * progress * showAnimation)
-
- build(rect, 0.0, topColor, topColor, bottomColor, bottomColor)
- }
-
- // Toggle fx
- renderer.filled.apply {
- val left = rect - Vec2d(rect.size.x, 0.0)
- val right = rect + Vec2d(rect.size.x, 0.0)
-
- val rect = lerp(activeAnimation, left, right)
- .clamp(rect)
- .shrink(shrinkAnimation)
-
- // 0.0 .. 1.0 .. 0.0 animation
- val alpha = 1.0 - (abs(activeAnimation - 0.5) * 2.0)
- val color = GuiSettings.mainColor.multAlpha(alpha * 0.6 * showAnimation)
-
- // "Tail" effect
- val leftColor = color.multAlpha(1.0 - toggleFxDirection)
- val rightColor = color.multAlpha(toggleFxDirection)
-
- build(rect, roundRadius, leftColor, rightColor, rightColor, leftColor, GuiSettings.shade)
- }
-
- if (renderHeight > 0.5) {
- scissor(settingsRect) {
- settingsLayer.onEvent(e)
- settingsRenderer.render()
- }
- }
-
- return
- }
- }
-
- super.onEvent(e)
- settingsLayer.onEvent(e)
- }
-
- private fun updateHeight() {
- settingsHeight = if (isOpen) {
- var lastStep = 0.0
- settingsLayer.children
- .filter(SettingButton<*, *>::visible)
- .sumOf { lastStep = it.listStep; it.size.y + it.listStep } - lastStep + super.listStep
- } else 0.0
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- when (e.button) {
- Mouse.Button.Left -> {
- module.toggle()
- }
-
- Mouse.Button.Right -> {
- // Don't let user spam
- val targetHeight = if (isOpen) settingsHeight else 0.0
- if (abs(targetHeight - renderHeight) > 1) return
-
- isOpen = !isOpen
- if (isOpen) settingsLayer.onEvent(GuiEvent.Show())
- updateHeight()
-
- val sound = if (isOpen) LambdaSound.SETTINGS_OPEN else LambdaSound.SETTINGS_CLOSE
- playSoundRandomly(sound.event)
- }
-
- else -> {}
- }
- }
-
- override fun equals(other: Any?) =
- (other as? ModuleButton)?.module == module
-
- override fun hashCode() =
- module.hashCode()
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt
deleted file mode 100644
index f37978b92..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/SettingButton.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons
-
-import com.lambda.config.AbstractSetting
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.button.ListButton
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.util.math.lerp
-
-abstract class SettingButton>(
- val setting: T,
- final override val owner: ChildLayer.Drawable, ModuleButton>,
-) : ListButton(owner) {
- override val text = setting.name
- protected var value by setting
-
- val visible; get() = setting.visibility()
- private var prevTickVisible = false
-
- private var visibilityAnimation by animation.exp(0.0, 1.0, 0.6, ::visible)
- override val showAnimation get() = lerp(visibilityAnimation, 0.0, super.showAnimation)
- override val renderHeightOffset get() = renderHeightAnimation + lerp(visibilityAnimation, -size.y, 0.0)
- override var activeAnimation = 0.0
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- if (e !is GuiEvent.Tick) return
-
- if (!prevTickVisible && visible) renderHeightAnimation = heightOffset
- prevTickVisible = visible
-
- if (!visible) unfocus()
- }
-
- open fun unfocus() {}
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt
deleted file mode 100644
index 5a50102f6..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BindButton.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.settings.complex.KeyBindSetting
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.button.InputBarOverlay
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.util.KeyCode
-import com.lambda.util.extension.displayValue
-import com.lambda.util.math.multAlpha
-
-class BindButton(
- setting: KeyBindSetting,
- owner: ChildLayer.Drawable, ModuleButton>,
-) : SettingButton(setting, owner) {
- private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive)
- private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) {
- override val pressAnimation get() = this@BindButton.pressAnimation
- override val interactAnimation get() = this@BindButton.interactAnimation
- override val hoverFontAnimation get() = this@BindButton.hoverFontAnimation
- override val showAnimation get() = this@BindButton.showAnimation
- override val isKeyBind = true
-
- override fun getText() = value.displayValue
- override fun setKeyValue(key: KeyCode) {
- value = key
- }
- }.apply(layer.children::add)
-
- override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation)
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
- layer.onEvent(e)
- }
-
- override fun unfocus() {
- inputBar.isActive = false
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings()
- inputBar.toggle()
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt
deleted file mode 100644
index 86d021aac..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/BooleanButton.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.settings.comparable.BooleanSetting
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.sound.LambdaSound
-import com.lambda.sound.SoundManager.playSoundRandomly
-import com.lambda.util.Mouse
-import com.lambda.util.math.Rect
-import com.lambda.util.math.Rect.Companion.inv
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-
-class BooleanButton(
- setting: BooleanSetting,
- owner: ChildLayer.Drawable, ModuleButton>,
-) : SettingButton(setting, owner) {
- private var active by animation.exp(0.0, 1.0, 0.6, ::value)
- private val zoomAnimation get() = lerp(showAnimation, 2.0, 0.0)
-
- private val checkboxRect
- get() = Rect(rect.rightTop - Vec2d(rect.size.y * 1.65, 0.0), rect.rightBottom)
- .shrink(1.0 + zoomAnimation)
- .moveFirst(Vec2d(0.0, 0.5)).moveSecond(Vec2d(0.0, -0.5))
-
- private val knobStart get() = Rect.basedOn(checkboxRect.leftTop, Vec2d.ONE * checkboxRect.size.y)
- private val knobEnd get() = Rect.basedOn(checkboxRect.rightBottom, Vec2d.ONE * checkboxRect.size.y * -1.0).inv()
- private val checkboxKnob get() = lerp(active, knobStart, knobEnd).shrink(1.0 + zoomAnimation + interactAnimation)
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- if (e is GuiEvent.Render) {
- // Checkbox Background
- renderer.filled.build(
- rect = checkboxRect,
- roundRadius = checkboxRect.size.y,
- color = GuiSettings.mainColor.multAlpha(showAnimation * (0.2 + active * 0.2)),
- shade = GuiSettings.shade
- )
-
- // Checkbox Knob
- renderer.filled.build(
- rect = checkboxKnob,
- roundRadius = checkboxKnob.size.y,
- color = GuiSettings.backgroundColor.multAlpha(showAnimation),
- shade = GuiSettings.shadeBackground
- )
- }
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- if (e.button != Mouse.Button.Left) return
- value = !value
-
- val sound = if (value) LambdaSound.BOOLEAN_SETTING_ON else LambdaSound.BOOLEAN_SETTING_OFF
- val pitch = if (value) 1.0 else 0.9
- playSoundRandomly(sound.event, pitch)
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt
deleted file mode 100644
index dfdcbfadf..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/EnumSlider.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.settings.comparable.EnumSetting
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.util.extension.displayValue
-import com.lambda.util.math.MathUtils.floorToInt
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.setAlpha
-import com.lambda.util.math.transform
-import java.awt.Color
-
-class EnumSlider>(
- setting: EnumSetting,
- owner: ChildLayer.Drawable, ModuleButton>,
-) : Slider>(setting, owner) {
- private val values = setting.enumValues
- private val enumSize = values.size
-
- override val progress get() = transform(value.ordinal.toDouble(), 0.0, enumSize - 1.0, 0.0, 1.0)
- private var valueSetByDrag = false
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- if (e is GuiEvent.Render) {
- // Enum entry name
- renderer.font.apply {
- val text = value.displayValue
- val progress = 1.0 - activeAnimation
- val scale = lerp(progress, 0.5, 1.0)
- val width = getWidth(text, scale)
- val position = Vec2d(rect.right, rect.center.y) - Vec2d(ClickGui.windowPadding + width, 0.0)
- val color = Color.WHITE.setAlpha(lerp(showAnimation, 0.0, progress))
-
- build(text, position, color, scale)
- }
- }
- }
-
- override fun setValueByProgress(progress: Double) {
- val entryIndex = (progress * enumSize).floorToInt().coerceIn(0, enumSize - 1)
- value = values[entryIndex]
- valueSetByDrag = true
- }
-
- override fun onPress(e: GuiEvent.MouseClick) {
- valueSetByDrag = false
- }
-
- override fun onRelease(e: GuiEvent.MouseClick) {
- if (valueSetByDrag) return
- playClickSound()
- setting.next()
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt
deleted file mode 100644
index d349743ba..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/NumberSlider.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.settings.NumericSetting
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.button.InputBarOverlay
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.util.Mouse
-import com.lambda.util.math.MathUtils.roundToStep
-import com.lambda.util.math.MathUtils.typeConvert
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.normalize
-
-class NumberSlider(
- setting: NumericSetting,
- owner: ChildLayer.Drawable, ModuleButton>,
-) : Slider>(
- setting, owner
-) where N : Number, N : Comparable {
- private val doubleRange get() = setting.range.let { it.start.toDouble()..it.endInclusive.toDouble() }
- override val progress get() = doubleRange.normalize(value.toDouble())
-
- private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive)
- private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) {
- override val pressAnimation get() = this@NumberSlider.pressAnimation
- override val interactAnimation get() = this@NumberSlider.interactAnimation
- override val hoverFontAnimation get() = this@NumberSlider.hoverFontAnimation
- override val showAnimation get() = this@NumberSlider.showAnimation
-
- override fun isCharAllowed(string: String, char: Char): Boolean {
- return when (char) {
- '.' -> char !in string
- '-' -> string.isEmpty()
- else -> char.isDigit()
- }
- }
-
- override fun getText() = "$setting".replace(',', '.') // "0,0".toDouble() is null
- override fun setStringValue(string: String) {
- string.toDoubleOrNull()?.let(::setValue)
- }
- }.apply(layer.children::add)
-
- override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation)
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
- layer.onEvent(e)
- }
-
- override fun unfocus() {
- inputBar.isActive = false
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- if (e.button != Mouse.Button.Right) return
- if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings()
- inputBar.toggle()
- }
-
- override fun slide() {
- if (!inputBar.isActive) super.slide()
- }
-
- override fun setValueByProgress(progress: Double) {
- setValue(
- lerp(
- progress,
- setting.range.start.toDouble(),
- setting.range.endInclusive.toDouble()
- )
- )
- }
-
- private fun setValue(valueIn: Double) {
- value = value.typeConvert(valueIn.roundToStep(setting.step.toDouble()))
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt
deleted file mode 100644
index e5bb367c7..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/Slider.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.AbstractSetting
-import com.lambda.graphics.animation.Animation.Companion.exp
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.module.modules.client.ClickGui
-import com.lambda.module.modules.client.GuiSettings
-import com.lambda.sound.LambdaSound
-import com.lambda.sound.SoundManager.playSound
-import com.lambda.util.Mouse
-import com.lambda.util.math.Vec2d
-import com.lambda.util.math.lerp
-import com.lambda.util.math.multAlpha
-import com.lambda.util.math.transform
-
-abstract class Slider>(
- setting: T, owner: ChildLayer.Drawable, ModuleButton>,
-) : SettingButton(setting, owner) {
- protected abstract val progress: Double
-
- // Force this slider to follow mouse when dragging instead of rounding to the closest setting value
- private val progressAnimation by animation.exp({ mouseX?.let(::getProgressByMouse) ?: progress }, 0.6)
- private val renderProgress get() = lerp(showAnimation, 0.0, progressAnimation)
-
- protected abstract fun setValueByProgress(progress: Double)
- private var lastPlayedValue = value
- private var lastPlayedTiming = 0L
-
- private var mouseX: Double? = null
- get() {
- if (activeButton != Mouse.Button.Left) field = null
- return field
- }
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- when (e) {
- is GuiEvent.Render -> {
- // Slider rect
- renderer.filled.build(
- rect = rect.moveSecond(Vec2d(-rect.size.x * (1.0 - renderProgress), 0.0)).shrink(shrinkAnimation),
- roundRadius = ClickGui.buttonRadius,
- color = GuiSettings.mainColor.multAlpha(showAnimation * 0.3),
- shade = GuiSettings.shade
- )
-
- slide()
- }
-
- is GuiEvent.MouseMove -> {
- mouseX = e.mouse.x
- }
- }
- }
-
- override fun onPress(e: GuiEvent.MouseClick) {
- super.onPress(e)
- mouseX = e.mouse.x
- }
-
- protected open fun slide() = mouseX?.let { mouseX ->
- setValueByProgress(getProgressByMouse(mouseX))
- playClickSound()
- }
-
- protected fun playClickSound() {
- val time = System.currentTimeMillis()
- if (lastPlayedValue == value || time - lastPlayedTiming < 50) return
-
- lastPlayedValue = value
- lastPlayedTiming = time
-
- playSound(LambdaSound.BUTTON_CLICK.event, lerp(progress, 0.9, 1.2))
- }
-
- private fun getProgressByMouse(mouseX: Double) =
- transform(mouseX, rect.left, rect.right, 0.0, 1.0).coerceIn(0.0, 1.0)
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt
deleted file mode 100644
index 91ba38c58..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/buttons/setting/StringButton.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.buttons.setting
-
-import com.lambda.config.settings.StringSetting
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.button.InputBarOverlay
-import com.lambda.gui.api.component.core.list.ChildLayer
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.gui.impl.clickgui.buttons.SettingButton
-import com.lambda.util.math.multAlpha
-
-class StringButton(
- setting: StringSetting,
- owner: ChildLayer.Drawable, ModuleButton>,
-) : SettingButton(setting, owner) {
- private val layer = ChildLayer.Drawable(owner.gui, this, owner.renderer, ::rect, InputBarOverlay::isActive)
- private val inputBar: InputBarOverlay = object : InputBarOverlay(renderer, layer) {
- override val pressAnimation get() = this@StringButton.pressAnimation
- override val interactAnimation get() = this@StringButton.interactAnimation
- override val hoverFontAnimation get() = this@StringButton.hoverFontAnimation
- override val showAnimation get() = this@StringButton.showAnimation
-
- override fun getText() = value
- override fun setStringValue(string: String) {
- value = string
- }
- }.apply(layer.children::add)
-
- override val textColor get() = super.textColor.multAlpha(1.0 - inputBar.activeAnimation)
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
- layer.onEvent(e)
- }
-
- override fun unfocus() {
- inputBar.isActive = false
- }
-
- override fun performClickAction(e: GuiEvent.MouseClick) {
- if (!inputBar.isActive) (owner.gui as? AbstractClickGui)?.unfocusSettings()
- inputBar.toggle()
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt
new file mode 100644
index 000000000..923065995
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/AnimatedChild.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.core
+
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.component.window.Window
+import com.lambda.gui.impl.clickgui.module.SettingLayout
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.util.math.MathUtils.toInt
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.lerp
+import com.lambda.util.math.setAlpha
+import com.lambda.util.math.transform
+import java.awt.Color
+import kotlin.math.pow
+
+abstract class AnimatedChild(
+ owner: Layout,
+ initialTitle: String = "Untitled",
+ initialPosition: Vec2d = Vec2d.ZERO,
+ initialSize: Vec2d = Vec2d(110, 350),
+ draggable: Boolean = false,
+ scrollable: Boolean = false,
+ minimizing: Minimizing = Minimizing.Disabled,
+ resizable: Boolean = false,
+ autoResize: AutoResize = AutoResize.Disabled
+) : Window(owner, initialTitle, initialPosition, initialSize, draggable, scrollable, minimizing, resizable, autoResize) {
+ private val window get() = owner?.owner as? Window
+ private val animatedWindow get() = owner?.owner as? AnimatedChild
+
+ // Hovering
+ protected val hovered get() = isHovered || isExpand || System.currentTimeMillis() - lastHover < 80
+ var hoverAnimation by animation.exp(0.0, 1.0, { if (hovered) 0.8 else 0.3 }, ::hovered)
+ private var lastHover = 0L // ToDo: replace with timer
+ open val shrink get() = 0.5 * lerp(openAnimation, 1.0, 0.5) * (hoverAnimation.pow(3) + pressAnimation)
+
+ protected var pressAnimation by animation.exp(0.0, 1.0, 0.6) { isPressed && content.selectedChild == null }
+ protected var openAnimation by animation.exp(1.0, 0.0, 0.6, ::isMinimized)
+
+ // Show animation for when the component is shown or hidden
+ open val isShown get() = true
+ private val isShownInternal get() = window?.isExpand != false && isShown
+ var showAnimation by animation.exp(0.0, 1.0, {
+ var speed = 0.7
+
+ if (lastIndex != 0) {
+ var start = speed
+ var end = speed
+ when (ClickGui.animationCurve) {
+ ClickGui.AnimationCurve.Normal -> start = ClickGui.smoothness
+ ClickGui.AnimationCurve.Static -> {}
+ ClickGui.AnimationCurve.Reverse -> end = ClickGui.smoothness
+ }
+ speed = transform(index.toDouble(), 0.0, lastIndex.toDouble(), start, end)
+ }
+
+ if ((this as? SettingLayout<*, *>)?.isVisible == true) speed *= 0.8
+
+ speed + isShownInternal.toInt() * 0.1
+ }) { isShownInternal }.apply {
+ if (window == null) this.setValue(1.0)
+ }; protected set
+
+ // Animation without index-based slowdown
+ var staticShowAnimation by animation.exp(0.0, 1.0, 0.7, ::isShown)
+
+ // Index for smooth "ordered" animation
+ var index = 0
+ var lastIndex = 0
+
+ protected val isLast
+ get() = index == lastIndex
+
+ override val isHovered: Boolean
+ get() = super.isHovered && isShown
+
+ override val renderSelf: Boolean
+ get() = showAnimation > 0.0 && super.renderSelf
+
+ init {
+ isMinimized = true
+ openAnimation = 0.0
+
+ onShow {
+ showAnimation = 0.0
+ staticShowAnimation = 0.0
+ }
+
+ onUpdate {
+ if (isHovered) lastHover = System.currentTimeMillis()
+ }
+
+ titleBar.textField.use {
+ textHAlignment = HAlign.LEFT
+
+ onUpdate {
+ offsetX = lerp(
+ showAnimation,
+ -5.0,
+ ClickGui.fontOffset + hoverAnimation * 2 - pressAnimation
+ )
+
+ scale = 1.0 *
+ lerp(showAnimation, 0.7, 1.0) *
+ lerp(pressAnimation, 1.0, 0.95)
+
+ color = Color.WHITE.setAlpha(showAnimation)
+ }
+ }
+
+ listOf(
+ titleBarBackground,
+ contentBackground,
+ outlineRect
+ ).forEach(Layout::destroy)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt
new file mode 100644
index 000000000..55464f924
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/core/SliderLayout.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.core
+
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.component.core.LayoutBuilder
+import com.lambda.gui.component.core.OutlineRect.Companion.outline
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.core.insertLayout
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.settings.SettingSlider
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.util.Mouse
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.multAlpha
+import com.lambda.util.math.setAlpha
+import com.lambda.util.math.transform
+import java.awt.Color
+
+class SliderLayout(
+ owner: Layout
+) : AnimatedChild(owner, "") {
+ // Not a great solution
+ private val setting = owner as? SettingSlider<*, *>
+ private val showAnim get() = setting?.showAnimation ?: showAnimation
+ private val hoverAnim get() = setting?.hoverAnimation ?: hoverAnimation
+ private val pressedBut get() = setting?.pressedButton ?: pressedButton
+
+ // Actions
+ private var getProgressBlock = { 0.0 }
+ private var setProgressBlock = { _: Double -> }
+
+ @LayoutBuilder
+ fun progress(action: SliderLayout.() -> Double) {
+ getProgressBlock = { action(this) }
+ }
+
+ @LayoutBuilder
+ fun onSlide(action: SliderLayout.(Double) -> Unit) {
+ setProgressBlock = { action(this, it) }
+ }
+
+ private val renderProgress get() = renderProgress0 * showAnim
+ private val renderProgress0 by animation.exp(0.7) {
+ if (pressedBut == Mouse.Button.Left) dragProgress else getProgressBlock()
+ }
+
+ private val bg = rect { // background
+ onUpdate {
+ rect = this@SliderLayout.rect
+ .moveFirst(Vec2d.RIGHT * ClickGui.fontOffset)
+ .moveSecond(Vec2d.LEFT * ClickGui.fontOffset)
+
+ shade = ClickGui.backgroundShade
+ setColor(Color.BLACK.setAlpha(0.25 * showAnim))
+ setRadius(100.0)
+ }
+ }
+
+ private val dragProgress: Double get() = transform(
+ mousePosition.x - bg.positionX,
+ 0.0, bg.width,
+ 0.0, 1.0
+ ).coerceIn(0.0, 1.0)
+
+ init {
+ (setting ?: this).onMouseMove {
+ if (pressedBut != Mouse.Button.Left) return@onMouseMove
+ setProgressBlock(dragProgress)
+ }
+
+ (setting ?: this).onMouse(Mouse.Button.Left, Mouse.Action.Click) {
+ if (pressedBut != Mouse.Button.Left) return@onMouse
+ setProgressBlock(dragProgress)
+ }
+
+ rect {
+ onUpdate { // progress
+ rect = bg.rect
+ width *= renderProgress
+
+ shade = ClickGui.backgroundShade
+ setColor(Color.WHITE.setAlpha(0.25 * showAnim))
+ setRadius(100.0)
+ }
+ }
+
+ outline {
+ onUpdate {
+ rect = bg.rect
+ val c = Color.BLACK.setAlpha(0.3 * showAnim)
+ val a = transform(renderProgress, 0.5, 1.0, 0.0, 1.0).coerceIn(0.0, 1.0)
+ setColorH(c, c.multAlpha(a))
+ roundRadius = 100.0
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a [SliderLayout].
+ */
+ @UIBuilder
+ fun Layout.slider(
+ block: SliderLayout.() -> Unit = {}
+ ) = SliderLayout(this).apply(children::add).apply(block)
+
+ /**
+ * Adds a [SliderLayout] behind given [layout]
+ */
+ @UIBuilder
+ fun Layout.sliderBehind(
+ layout: Layout,
+ block: SliderLayout.() -> Unit = {}
+ ) = SliderLayout(this).insertLayout(this, layout, false).apply(block)
+
+ /**
+ * Adds a [SliderLayout] over given [layout]
+ */
+ @UIBuilder
+ fun Layout.sliderOver(
+ layout: Layout,
+ block: SliderLayout.() -> Unit = {}
+ ) = SliderLayout(this).insertLayout(this, layout, true).apply(block)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt
new file mode 100644
index 000000000..b07718345
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/ModuleLayout.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module
+
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.module.Module
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.GuiManager.layoutOf
+import com.lambda.gui.component.core.FilledRect
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.component.core.FilledRect.Companion.rectBehind
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.component.window.Window
+import com.lambda.gui.impl.clickgui.ModuleWindow
+import com.lambda.gui.impl.clickgui.core.AnimatedChild
+import com.lambda.util.Mouse
+import com.lambda.util.math.*
+import java.awt.Color
+
+class ModuleLayout(
+ owner: Layout,
+ module: Module,
+ initialPosition: Vec2d = Vec2d.ZERO,
+ initialSize: Vec2d = Vec2d(100, 18)
+) : AnimatedChild(
+ owner,
+ module.name,
+ initialPosition, initialSize,
+ false, false, Minimizing.Absolute, false,
+ AutoResize.ForceEnabled
+) {
+ private val cursorController = cursorController()
+
+ private var enableAnimation by animation.exp(0.0, 1.0, 0.6, module::isEnabled)
+
+ val backgroundRect = rectBehind(titleBar) { // base rect with lowest y to avoid children overlying
+ onUpdate {
+ rect = this@ModuleLayout.rect.shrink(shrink)
+ shade = ClickGui.backgroundShade
+
+ val openRev = 1.0 - openAnimation // 1.0 <-> 0.0
+ val openRevSigned = openRev * 2 - 1 // 1.0 <-> -1.0
+ val enableRev = 1.0 - enableAnimation // 1.0 <-> 0.0
+
+ var progress = enableAnimation
+
+ // hover: +0.1 to alpha if minimized, -0.1 to alpha if maximized and enabled
+ progress += hoverAnimation * ClickGui.moduleHoverAccent *
+ lerp(enableAnimation, 1.0, openRevSigned)
+
+ // +0.4 to alpha if opened and disabled
+ progress += openAnimation * ClickGui.moduleOpenAccent * enableRev
+
+ // interpolate and set the color
+ setColor(
+ lerp(progress,
+ ClickGui.moduleDisabledColor,
+ ClickGui.moduleEnabledColor
+ ).multAlpha(showAnimation)
+ )
+
+ setRadius(hoverAnimation)
+
+ if (isLast && ClickGui.autoResize) {
+ leftBottomRadius = ClickGui.roundRadius - (ClickGui.padding + shrink)
+ rightBottomRadius = leftBottomRadius
+ }
+ }
+
+ rect { // hover fx
+ onUpdate {
+ val base = this@rect.owner as FilledRect
+
+ position = base.position
+ size = base.size
+ shade = base.shade
+
+ setRadius(
+ base.leftTopRadius,
+ base.rightTopRadius,
+ base.rightBottomRadius,
+ base.leftBottomRadius
+ )
+
+ val hoverColor = Color.WHITE.setAlpha(
+ ClickGui.moduleHoverAccent * hoverAnimation * (1.0 - openAnimation) * showAnimation
+ )
+
+ setColorH(hoverColor.setAlpha(0.0), hoverColor)
+ }
+ }
+ }
+
+ init {
+ backgroundTint()
+
+ onUpdate {
+ positionX = owner.positionX + ClickGui.padding
+ width = owner.width - ClickGui.padding * 2
+ }
+
+ titleBar.use {
+ onUpdate {
+ height = ClickGui.moduleHeight
+ }
+
+ onMouseAction(Mouse.Button.Left) {
+ module.toggle()
+ }
+ }
+
+ onShow {
+ enableAnimation = 0.0
+ hoverAnimation = 0.0
+ isMinimized = true
+ }
+
+ onTick {
+ val cursor = if (titleBar.isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow
+ cursorController.setCursor(cursor)
+ }
+
+ onWindowExpand {
+ if (ClickGui.multipleSettingWindows) return@onWindowExpand
+
+ val base = owner // window content
+ .owner // window
+ ?.owner // environment with windows
+ ?: return@onWindowExpand
+
+ base.children.filterIsInstance().forEach { window ->
+ window.content.children.filterIsInstance().forEach { module ->
+ if (module != this) module.isMinimized = true
+ }
+ }
+ }
+
+ val settings = module.settings.map { setting ->
+ content.layoutOf(setting)
+ }.filterIsInstance>()
+
+ val minimizeSettings = {
+ settings.forEach {
+ it.isMinimized = true
+ }
+ }
+
+ onWindowExpand { minimizeSettings() }
+ onWindowMinimize { minimizeSettings() }
+ content.listify()
+ }
+
+ companion object {
+ /**
+ * Creates a [ModuleLayout] - visual representation of the [Module]
+ */
+ @UIBuilder
+ fun Layout.moduleLayout(module: Module) =
+ ModuleLayout(this, module).apply(children::add)
+
+ /**
+ * Used to dark the background of the settings a bit
+ *
+ * Not for external usage
+ */
+ @UIBuilder
+ fun Window.backgroundTint(tintTitleBar: Boolean = false) {
+ check(this is SettingLayout<*, *> || this is ModuleLayout || this is ModuleWindow)
+
+ val base = this@backgroundTint
+
+ rectBehind(content) {
+ onUpdate {
+ rect = if (tintTitleBar) base.rect
+ else Rect(titleBar.leftBottom, base.rightBottom)
+
+ setColor(Color.BLACK.setAlpha(0.08 * heightAnimation))
+
+ val round = (base as? ModuleLayout?)?.backgroundRect
+ ?: (base as? ModuleWindow)?.contentBackground
+
+ round?.let {
+ leftBottomRadius = it.leftBottomRadius
+ rightBottomRadius = it.rightBottomRadius
+ }
+ }
+
+ val bg = this
+
+ rect { // top shadow
+ onUpdate {
+ position = bg.position
+ width = bg.width
+ height = titleBar.height * 0.2
+
+ setColorV(Color.BLACK.setAlpha(0.1 * heightAnimation), Color.BLACK.setAlpha(0.0))
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt
new file mode 100644
index 000000000..20183318b
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/SettingLayout.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module
+
+import com.lambda.config.AbstractSetting
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.ModuleLayout.Companion.backgroundTint
+import com.lambda.gui.impl.clickgui.core.AnimatedChild
+import com.lambda.util.math.*
+
+/**
+ * A base class for setting layouts.
+ */
+abstract class SettingLayout > (
+ owner: Layout,
+ val setting: T,
+ private val expandable: Boolean = false
+) : AnimatedChild(
+ owner,
+ setting.name,
+ Vec2d.ZERO, Vec2d.ZERO,
+ false, false,
+ if (expandable) Minimizing.Absolute else Minimizing.Disabled,
+ false,
+ AutoResize.ForceEnabled
+) {
+ protected val cursorController = cursorController()
+
+ var settingDelegate by setting
+ val isVisible get() = setting.visibility()
+
+ override val isShown: Boolean get() = super.isShown && isVisible
+
+
+
+ init {
+ isMinimized = true
+
+ onUpdate {
+ width = owner.width
+ }
+
+ titleBar.onUpdate {
+ height = ClickGui.settingsHeight
+ }
+
+ if (!expandable) {
+ onUpdate {
+ height = titleBar.height
+ }
+ content.destroy()
+ } else {
+ backgroundTint(true)
+ }
+
+ titleBar.textField.onUpdate {
+ scale *= 0.92
+ }
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt
new file mode 100644
index 000000000..35fa632a0
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/BooleanButton.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module.settings
+
+import com.lambda.config.settings.comparable.BooleanSetting
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.component.core.OutlineRect.Companion.outline
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.SettingLayout
+import com.lambda.util.Mouse
+import com.lambda.util.math.Rect
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.lerp
+import com.lambda.util.math.setAlpha
+import java.awt.Color
+
+class BooleanButton(
+ owner: Layout,
+ setting: BooleanSetting
+) : SettingLayout(owner, setting) {
+ private val activeAnimation by animation.exp(0.0, 1.0, 0.6, ::settingDelegate)
+
+ init {
+ val checkBox = rect { // Checkbox
+ val shrink = 3.0
+ setRadius(100.0)
+
+ onUpdate {
+ val rb = this@BooleanButton.rightBottom
+ val h = this@BooleanButton.height
+
+ rect = Rect(rb - Vec2d(h * 1.65, h), rb)
+ .shrink(shrink + (1.0 - showAnimation)) +
+ Vec2d.RIGHT * lerp(showAnimation, 5.0, -ClickGui.fontOffset + shrink)
+
+ setColor(lerp(activeAnimation, Color.BLACK, Color.WHITE).setAlpha(0.25 * showAnimation))
+ shade = ClickGui.backgroundShade
+ }
+
+ onTick {
+ cursorController.setCursor(
+ if (isHovered) Mouse.Cursor.Pointer else Mouse.Cursor.Arrow
+ )
+ }
+ }
+
+ rect { // Knob
+ setRadius(100.0)
+
+ onUpdate {
+ val knobStart = Rect.basedOn(checkBox.leftTop, Vec2d.ONE * checkBox.size.y)
+ val knobEnd = Rect(checkBox.rightBottom - checkBox.size.y, checkBox.rightBottom)
+
+ rect = lerp(
+ lerp(showAnimation, 1.0 - activeAnimation, activeAnimation),
+ knobStart,
+ knobEnd
+ ).shrink(1.0)
+
+ shade = ClickGui.backgroundShade
+
+ setColor(lerp(activeAnimation, Color.WHITE, Color.BLACK).setAlpha(lerp(activeAnimation, 0.25, 0.4) * showAnimation))
+ }
+ }
+
+ outline {
+ onUpdate {
+ rect = checkBox.rect
+ roundRadius = 100.0
+ glowRadius = 5.0
+ setColor(Color.BLACK.setAlpha(0.1 * showAnimation))
+ }
+ }
+
+ onMouseAction(Mouse.Button.Left) {
+ setting.value = !setting.value
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a [BooleanButton] - visual representation of the [BooleanSetting]
+ */
+ @UIBuilder
+ fun Layout.booleanSetting(setting: BooleanSetting) =
+ BooleanButton(this, setting).apply(children::add)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt
new file mode 100644
index 000000000..e6ae47828
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/EnumSlider.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module.settings
+
+import com.lambda.config.settings.comparable.EnumSetting
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.math.MathUtils.floorToInt
+import com.lambda.util.math.transform
+
+class EnumSlider >(
+ owner: Layout,
+ setting: EnumSetting
+) : SettingSlider>(owner, setting) {
+ override val settingValue: String
+ get() = settingDelegate.name
+
+ init {
+ slider.progress {
+ transform(
+ value = settingDelegate.ordinal.toDouble(),
+ ogStart = 0.0, ogEnd = setting.enumValues.lastIndex.toDouble(),
+ nStart = 0.0, nEnd = 1.0
+ )
+ }
+
+ slider.onSlide {
+ settingDelegate = setting.enumValues.let { entries ->
+ entries[(it * entries.size)
+ .floorToInt()
+ .coerceIn(0, entries.size - 1)]
+ }
+ }
+ }
+
+ companion object {
+ /**
+ * Creates an [EnumSlider] - visual representation of the [EnumSetting]
+ */
+ @UIBuilder
+ fun > Layout.enumSetting(setting: EnumSetting) =
+ EnumSlider(this, setting).apply(children::add)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt
new file mode 100644
index 000000000..ba786a44a
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/KeybindPicker.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module.settings
+
+import com.lambda.config.settings.complex.KeyBindSetting
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.core.TextField.Companion.textField
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.ModuleLayout
+import com.lambda.gui.impl.clickgui.module.SettingLayout
+import com.lambda.util.KeyCode
+import com.lambda.util.Mouse
+import com.lambda.util.extension.displayValue
+
+class KeybindPicker(
+ owner: Layout,
+ setting: KeyBindSetting
+) : SettingLayout(owner, setting) {
+ private var isListening = false
+
+ init {
+ textField {
+ onUpdate {
+ mergeFrom(titleBar.textField)
+ text = if (isListening) "..." else setting.value.displayValue
+ textHAlignment = HAlign.RIGHT
+ }
+ }
+
+ onMouseAction(Mouse.Button.Left) {
+ isListening = !isListening
+ }
+
+ onKeyPress { key ->
+ if (!isListening) return@onKeyPress
+
+ settingDelegate = key
+ isListening = false
+ }
+
+ onShow {
+ isListening = false
+ }
+
+ onTick {
+ val module = (owner.owner as? ModuleLayout) ?: return@onTick
+ if (module.isMinimized) isListening = false
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a [KeybindPicker] - visual representation of the [KeyBindSetting]
+ */
+ @UIBuilder
+ fun Layout.keybindSetting(setting: KeyBindSetting) =
+ KeybindPicker(this, setting).apply(children::add)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt
new file mode 100644
index 000000000..6999a7950
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/NumberSlider.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module.settings
+
+import com.lambda.config.settings.NumericSetting
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.util.math.MathUtils.roundToStep
+import com.lambda.util.math.MathUtils.typeConvert
+import com.lambda.util.math.lerp
+import com.lambda.util.math.transform
+
+class NumberSlider (
+ owner: Layout, setting: NumericSetting
+) : SettingSlider>(owner, setting) where V : Number, V : Comparable {
+ private val min = setting.range.start.toDouble()
+ private val max = setting.range.endInclusive.toDouble()
+
+ override val settingValue: String
+ get() = "${setting.value}${setting.unit}"
+
+ init {
+ slider.progress {
+ transform(
+ settingDelegate.toDouble(),
+ min, max,
+ 0.0, 1.0
+ )
+ }
+
+ slider.onSlide {
+ settingDelegate = settingDelegate.typeConvert(
+ lerp(it, min, max).roundToStep(setting.step).toDouble()
+ )
+ }
+ }
+
+ companion object {
+ /**
+ * Creates an [NumberSlider] - visual representation of the [NumericSetting]
+ */
+ @UIBuilder
+ fun Layout.numericSetting(setting: NumericSetting) where T : Number, T : Comparable =
+ NumberSlider(this, setting).apply(children::add)
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt
new file mode 100644
index 000000000..7b43339d7
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/settings/SettingSlider.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.module.settings
+
+import com.lambda.config.AbstractSetting
+import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.VAlign
+import com.lambda.gui.component.core.TextField.Companion.textField
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.core.SliderLayout.Companion.sliderBehind
+import com.lambda.gui.impl.clickgui.module.SettingLayout
+import com.lambda.module.modules.client.ClickGui
+import com.lambda.util.math.lerp
+
+abstract class SettingSlider >(
+ owner: Layout, setting: T
+) : SettingLayout(owner, setting, false) {
+ abstract val settingValue: String
+
+ private var changeAnimation by animation.exp(0.0, 1.0, 0.5) { true }
+
+ private val sliderHeight = 3.0
+
+ protected val slider = sliderBehind(titleBar) {
+ val sl = this@SettingSlider
+
+ onUpdate {
+ positionX = sl.positionX
+ positionY = sl.positionY + sl.height * 0.75 - sliderHeight * 0.5
+ width = sl.width
+ height = sliderHeight
+ }
+ }
+
+ init {
+ titleBar.use {
+ onUpdate {
+ height = ClickGui.settingsHeight * 1.25
+ }
+
+ textField.onUpdate {
+ offsetY = 0.25 * height
+ textVAlignment = VAlign.TOP
+ }
+
+ textField {
+ var lastValue: String
+
+ onUpdate {
+ lastValue = text
+ mergeFrom(textField)
+ text = settingValue
+
+ if (lastValue != text) changeAnimation = 0.0
+ textHAlignment = HAlign.RIGHT
+ scale *= lerp(changeAnimation, 1.1, 1.0)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt
new file mode 100644
index 000000000..290a5c7ea
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/settings/UnitButton.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.gui.impl.clickgui.settings
+
+import com.lambda.config.settings.FunctionSetting
+import com.lambda.gui.component.core.UIBuilder
+import com.lambda.gui.component.layout.Layout
+import com.lambda.gui.impl.clickgui.module.SettingLayout
+import com.lambda.util.Mouse
+
+class UnitButton (
+ owner: Layout,
+ setting: FunctionSetting,
+) : SettingLayout<() -> T, FunctionSetting>(owner, setting) {
+ init {
+ onMouse(Mouse.Button.Left, Mouse.Action.Click) {
+ setting.value()
+ }
+ }
+
+ companion object {
+ /**
+ * Creates a [UnitButton] - visual representation of the [FunctionSetting]
+ */
+ @UIBuilder
+ fun Layout.unitSetting(setting: FunctionSetting) =
+ UnitButton(this, setting).apply(children::add)
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt
deleted file mode 100644
index 2aa69c98e..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/ModuleWindow.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.windows
-
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.api.component.ListWindow
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.module.Module
-
-abstract class ModuleWindow(
- override var title: String,
- override var width: Double = 110.0,
- override var height: Double = 300.0,
- gui: AbstractClickGui,
-) : ListWindow(gui) {
- private var lastUpdate = 0L
-
- abstract fun getModuleList(): Collection
-
- private fun updateModules() {
- val time = System.currentTimeMillis()
- if (time - lastUpdate < 1000L) return
- lastUpdate = time
-
- contentComponents.apply {
- val modules = getModuleList().filter((gui as AbstractClickGui).moduleFilter)
-
- // Add missing module buttons
- modules.filter { module ->
- children.all { button ->
- button.module != module
- }
- }.map { ModuleButton(it, contentComponents) }
- .forEach(contentComponents.children::add)
-
- // Remove deleted modules
- children.removeIf {
- it.module !in modules
- }
- }
- }
-
- override fun onEvent(e: GuiEvent) {
- if (e is GuiEvent.Show || e is GuiEvent.Tick) updateModules()
-
- if (e is GuiEvent.Tick) {
- contentComponents.children.sortBy {
- it.module.name
- }
- }
-
- super.onEvent(e)
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt
deleted file mode 100644
index d30e7de98..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/CustomModuleWindow.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.clickgui.windows.tag
-
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.windows.ModuleWindow
-import com.lambda.module.Module
-
-class CustomModuleWindow(
- override var title: String = "Untitled",
- val modules: MutableList = mutableListOf(),
- gui: AbstractClickGui,
-) : ModuleWindow(title, gui = gui) {
- override fun getModuleList() = modules
-}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt b/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt
deleted file mode 100644
index 4cdc026b6..000000000
--- a/common/src/main/kotlin/com/lambda/gui/impl/hudgui/LambdaHudGui.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2024 Lambda
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package com.lambda.gui.impl.hudgui
-
-import com.lambda.gui.HudGuiConfigurable
-import com.lambda.gui.api.GuiEvent
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.LambdaClickGui
-import com.lambda.module.HudModule
-import com.lambda.module.Module
-import com.lambda.module.ModuleRegistry
-import com.lambda.util.Mouse
-import com.lambda.util.math.Vec2d
-
-object LambdaHudGui : AbstractClickGui("HudGui") {
- override val moduleFilter: (Module) -> Boolean = {
- it is HudModule
- }
-
- override val configurable get() = HudGuiConfigurable
- private val hudModules get() = ModuleRegistry.modules.filterIsInstance()
-
- private var dragInfo: Pair? = null
-
- override fun onEvent(e: GuiEvent) {
- super.onEvent(e)
-
- when (e) {
- is GuiEvent.Show -> {
- dragInfo = null
-
- setCloseTask {
- LambdaClickGui.show()
- }
- }
-
- is GuiEvent.MouseMove -> {
- if (closing) dragInfo = null
-
- dragInfo?.let {
- it.second.position = e.mouse - it.first
- }
- }
-
- is GuiEvent.MouseClick -> {
- dragInfo = null
-
- if (hoveredWindow == null &&
- e.action == Mouse.Action.Click &&
- e.button == Mouse.Button.Left
- ) hudModules.filter(Module::isEnabled).firstOrNull { e.mouse in it.rect }?.let {
- dragInfo = e.mouse - it.position to it
- }
- }
- }
- }
-}
diff --git a/common/src/main/kotlin/com/lambda/module/HudModule.kt b/common/src/main/kotlin/com/lambda/module/HudModule.kt
index 505eb6437..15cdb1ec0 100644
--- a/common/src/main/kotlin/com/lambda/module/HudModule.kt
+++ b/common/src/main/kotlin/com/lambda/module/HudModule.kt
@@ -21,9 +21,10 @@ import com.lambda.event.events.RenderEvent
import com.lambda.event.events.TickEvent
import com.lambda.event.listener.SafeListener.Companion.listen
import com.lambda.graphics.animation.AnimationTicker
-import com.lambda.gui.api.RenderLayer
-import com.lambda.gui.api.component.core.DockingRect
+import com.lambda.gui.component.DockingRect
import com.lambda.module.tag.ModuleTag
+import com.lambda.gui.component.HAlign
+import com.lambda.gui.component.VAlign
import com.lambda.util.KeyCode
import com.lambda.util.math.Vec2d
@@ -35,8 +36,6 @@ abstract class HudModule(
enabledByDefault: Boolean = false,
defaultKeybind: KeyCode = KeyCode.UNBOUND,
) : Module(name, description, defaultTags, alwaysListening, enabledByDefault, defaultKeybind) {
- private val renderCallables = mutableListOf Unit>()
-
protected abstract val width: Double
protected abstract val height: Double
@@ -71,22 +70,10 @@ abstract class HudModule(
val rect by rectHandler::rect
val animation = AnimationTicker()
- private val renderer = RenderLayer()
-
- protected fun onRender(block: RenderLayer.() -> Unit) =
- renderCallables.add(block)
+ protected fun onRender(block: () -> Unit) =
+ listen { block() }
init {
- listen { event ->
- rectHandler.screenSize = event.screenSize
-
- renderCallables.forEach { function ->
- function(renderer)
- }
-
- renderer.render()
- }
-
listen {
animation.tick()
}
diff --git a/common/src/main/kotlin/com/lambda/module/Module.kt b/common/src/main/kotlin/com/lambda/module/Module.kt
index 76680ba8e..48a42e148 100644
--- a/common/src/main/kotlin/com/lambda/module/Module.kt
+++ b/common/src/main/kotlin/com/lambda/module/Module.kt
@@ -31,11 +31,11 @@ import com.lambda.event.listener.Listener
import com.lambda.event.listener.SafeListener
import com.lambda.event.listener.SafeListener.Companion.listen
import com.lambda.event.listener.UnsafeListener
-import com.lambda.gui.impl.clickgui.buttons.ModuleButton
-import com.lambda.module.modules.client.ClickGui
+import com.lambda.module.hud.ModuleList
import com.lambda.module.tag.ModuleTag
import com.lambda.sound.LambdaSound
import com.lambda.sound.SoundManager.playSoundRandomly
+import com.lambda.util.Communication.info
import com.lambda.util.KeyCode
import com.lambda.util.Nameable
@@ -117,8 +117,9 @@ abstract class Module(
) : Nameable, Muteable, Configurable(ModuleConfig) {
private val isEnabledSetting = setting("Enabled", enabledByDefault, visibility = { false })
private val keybindSetting = setting("Keybind", defaultKeybind)
- private val isVisible = setting("Visible", true)
- val customTags = setting("Tags", setOf(), visibility = { false })
+ val isVisible = setting("Visible", true) { ModuleList.isEnabled }
+ val reset by setting("Reset", { settings.forEach { it.reset() }; this@Module.info("Settings set to default") })
+ val customTags = setting("Tags", setOf()) { false }
var isEnabled by isEnabledSetting
val isDisabled get() = !isEnabled
@@ -151,6 +152,14 @@ abstract class Module(
onDisable {
playSoundRandomly(LambdaSound.MODULE_OFF.event)
}
+
+ onEnable {
+ playSoundRandomly(LambdaSound.MODULE_ON.event)
+ }
+
+ onDisable {
+ playSoundRandomly(LambdaSound.MODULE_OFF.event)
+ }
}
fun enable() {
diff --git a/common/src/main/kotlin/com/lambda/module/hud/Coordinates.kt b/common/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
index 35388f12c..d0f7a5fd1 100644
--- a/common/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/Coordinates.kt
@@ -18,6 +18,7 @@
package com.lambda.module.hud
import com.lambda.context.SafeContext
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
import com.lambda.module.HudModule
import com.lambda.module.tag.ModuleTag
import com.lambda.threading.runSafe
@@ -47,7 +48,7 @@ object Coordinates : HudModule(
init {
onRender {
runSafe {
- font.build(text, position)
+ drawString(text, position)
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt b/common/src/main/kotlin/com/lambda/module/hud/GifTest.kt
similarity index 60%
rename from common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt
rename to common/src/main/kotlin/com/lambda/module/hud/GifTest.kt
index 4e2c33e1b..b4c8fe833 100644
--- a/common/src/main/kotlin/com/lambda/gui/HudGuiConfigurable.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/GifTest.kt
@@ -15,13 +15,25 @@
* along with this program. If not, see .
*/
-package com.lambda.gui
+package com.lambda.module.hud
-import com.lambda.gui.impl.hudgui.LambdaHudGui
+import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture
+import com.lambda.graphics.texture.TextureOwner.uploadGif
+import com.lambda.module.HudModule
import com.lambda.module.tag.ModuleTag
-object HudGuiConfigurable : AbstractGuiConfigurable(
- LambdaHudGui, ModuleTag.hudDefaults, "hudgui"
+object GifTest : HudModule(
+ name = "GifTest",
+ defaultTags = setOf(ModuleTag.CLIENT),
) {
- override fun load() = "Loaded HUD GUI Configurable"
+ val test = uploadGif("chika.gif")
+
+ override val width = 100.0
+ override val height = 100.0
+
+ init {
+ onRender {
+ drawTexture(test, rect)
+ }
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt b/common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt
similarity index 54%
rename from common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt
rename to common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt
index db45d678e..ee2037817 100644
--- a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/windows/tag/TagWindow.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/ModuleList.kt
@@ -15,24 +15,27 @@
* along with this program. If not, see .
*/
-package com.lambda.gui.impl.clickgui.windows.tag
+package com.lambda.module.hud
-import com.lambda.gui.impl.AbstractClickGui
-import com.lambda.gui.impl.clickgui.windows.ModuleWindow
-import com.lambda.gui.impl.hudgui.LambdaHudGui
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
import com.lambda.module.HudModule
-import com.lambda.module.Module
import com.lambda.module.ModuleRegistry
import com.lambda.module.tag.ModuleTag
+import com.lambda.util.math.Vec2d
-class TagWindow(
- val tag: ModuleTag,
- owner: AbstractClickGui,
-) : ModuleWindow(tag.name, gui = owner) {
- val isHudWindow = gui is LambdaHudGui
- private val rawFilter = { m: Module -> m is HudModule }
- private val filter get() = if (isHudWindow) rawFilter else { m: Module -> !rawFilter(m) }
+object ModuleList : HudModule(
+ name = "ModuleList",
+ defaultTags = setOf(ModuleTag.CLIENT),
+) {
+ override val width = 200.0
+ override val height = 200.0
- override fun getModuleList() = ModuleRegistry.modules
- .filter { it.defaultTags.firstOrNull() == tag && filter(it) }
+ init {
+ onRender {
+ val enabled = ModuleRegistry.modules
+ .filter { it.isEnabled }
+ .filter { it.isVisible.value }
+ drawString(enabled.joinToString("\n") { "${it.name} [${it.keybind.name}]" }, Vec2d.ZERO)
+ }
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/module/hud/TPS.kt b/common/src/main/kotlin/com/lambda/module/hud/TPS.kt
index fa5b0b683..41e4c807c 100644
--- a/common/src/main/kotlin/com/lambda/module/hud/TPS.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/TPS.kt
@@ -17,6 +17,7 @@
package com.lambda.module.hud
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
import com.lambda.module.HudModule
import com.lambda.module.tag.ModuleTag
import com.lambda.util.Formatting.string
@@ -39,7 +40,7 @@ object TPS : HudModule(
init {
onRender {
- font.build(text, position)
+ drawString(text, position)
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt
index b8c41ed20..f4fd69552 100644
--- a/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/TaskFlowHUD.kt
@@ -17,6 +17,7 @@
package com.lambda.module.hud
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
import com.lambda.module.HudModule
import com.lambda.module.tag.ModuleTag
import com.lambda.task.RootTask
@@ -31,9 +32,7 @@ object TaskFlowHUD : HudModule(
init {
onRender {
- RootTask.toString().lines().forEachIndexed { index, line ->
- font.build(line, Vec2d(position.x, position.y + index * (font.getHeight(font.scaleMultiplier) + 2.0)))
- }
+ drawString(RootTask.toString(), Vec2d.ZERO)
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt
index 57d7f36d0..600a21271 100644
--- a/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/TickShiftCharge.kt
@@ -18,6 +18,10 @@
package com.lambda.module.hud
import com.lambda.graphics.animation.Animation.Companion.exp
+import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer
+import com.lambda.graphics.renderer.gui.rect.FilledRectRenderer.filledRect
+import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer
+import com.lambda.graphics.renderer.gui.rect.OutlineRectRenderer.outlineRect
import com.lambda.module.HudModule
import com.lambda.module.modules.client.ClickGui
import com.lambda.module.modules.client.GuiSettings
@@ -35,39 +39,41 @@ object TickShiftCharge : HudModule(
private val isActive get() = TickShift.isEnabled && TickShift.isActive && TickShift.boost
private val activeAnimation by animation.exp(0.0, 1.0, 0.6, ::isActive)
- private val progress
- get() = if (!TickShift.isActive) 0.0
- else (TickShift.balance / TickShift.maxBalance.toDouble()).coerceIn(0.0..1.0)
+ private val renderProgress by animation.exp(0.8) {
+ if (!TickShift.isActive) return@exp 0.0
- private val renderProgress by animation.exp(::progress, 0.8)
+ (TickShift.balance / TickShift.maxBalance.toDouble()).coerceIn(0.0..1.0)
+ }
override val width = 70.0
override val height = 14.0
init {
onRender {
- filled.build(
+ filledRect(
rect = rect,
- roundRadius = ClickGui.windowRadius,
+ roundRadius = ClickGui.roundRadius,
color = GuiSettings.backgroundColor,
shade = GuiSettings.shadeBackground
)
val padding = 1.0
- filled.build(
+ filledRect(
rect = Rect.basedOn(rect.leftTop, rect.size.x * renderProgress, rect.size.y).shrink(padding),
- roundRadius = ClickGui.windowRadius - padding,
+ roundRadius = ClickGui.roundRadius - padding,
color = GuiSettings.mainColor.multAlpha(0.3),
shade = true
)
- outline.build(
- rect = rect,
- roundRadius = ClickGui.windowRadius,
- color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(activeAnimation),
- glowRadius = ClickGui.glowRadius * activeAnimation,
- shade = true
- )
+ if (ClickGui.outline) {
+ outlineRect(
+ rect = rect,
+ roundRadius = ClickGui.roundRadius,
+ color = (if (GuiSettings.shadeBackground) Color.WHITE else primaryColor).multAlpha(activeAnimation),
+ glowRadius = ClickGui.outlineWidth * activeAnimation,
+ shade = true
+ )
+ }
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt
index 640d5caaf..794738eb0 100644
--- a/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt
+++ b/common/src/main/kotlin/com/lambda/module/hud/Watermark.kt
@@ -19,7 +19,7 @@ package com.lambda.module.hud
import com.lambda.graphics.renderer.gui.TextureRenderer.drawTexture
import com.lambda.graphics.renderer.gui.TextureRenderer.drawTextureShaded
-import com.lambda.graphics.texture.MipmapTexture
+import com.lambda.graphics.texture.TextureOwner.upload
import com.lambda.module.HudModule
import com.lambda.module.tag.ModuleTag
@@ -32,8 +32,8 @@ object Watermark : HudModule(
override val width = 50.0
override val height = 50.0
- private val normalTexture = MipmapTexture.fromResource("textures/lambda.png")
- private val monoTexture = MipmapTexture.fromResource("textures/lambda_mono.png")
+ private val normalTexture = upload("textures/lambda.png")
+ private val monoTexture = upload("textures/lambda_mono.png")
init {
onRender {
diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt
index 6bfdbfa80..f3db6b239 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/client/ClickGui.kt
@@ -17,61 +17,85 @@
package com.lambda.module.modules.client
-import com.lambda.event.events.ClientEvent
-import com.lambda.event.events.KeyboardEvent
-import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
-import com.lambda.gui.impl.clickgui.LambdaClickGui
-import com.lambda.gui.impl.hudgui.LambdaHudGui
+import com.lambda.Lambda.mc
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
+import com.lambda.gui.ScreenLayout.Companion.gui
+import com.lambda.gui.component.core.FilledRect.Companion.rect
+import com.lambda.gui.impl.clickgui.ModuleWindow.Companion.moduleWindow
import com.lambda.util.KeyCode
+import com.lambda.util.math.Vec2d
+import com.lambda.util.math.setAlpha
+import java.awt.Color
object ClickGui : Module(
name = "ClickGui",
- description = "Sexy",
- defaultTags = setOf(ModuleTag.CLIENT),
- defaultKeybind = KeyCode.RIGHT_SHIFT
+ description = "sexy again",
+ defaultTags = setOf(ModuleTag.CLIENT)
) {
- // General
- val windowRadius by setting("Window Radius", 2.0, 0.0..10.0, 0.1)
- val glowRadius by setting("Glow Radius", 2.0, 0.0..20.0, 0.1)
- val buttonRadius by setting("Button Radius", 0.0, 0.0..10.0, 0.1)
- val windowPadding by setting("Window Padding", 2.0, 0.0..10.0, 0.1)
- val buttonHeight by setting("Button Height", 11.0, 8.0..20.0, 0.1)
- val buttonStep by setting("Button Step", 0.0, 0.0..5.0, 0.1)
- val settingsFontScale by setting("Settings Font Scale", 0.92, 0.5..1.0, 0.01)
+ val titleBarHeight by setting("Title Bar Height", 18.0, 10.0..25.0, 0.1)
+ val moduleHeight by setting("Module Height", 16.0, 10.0..25.0, 0.1)
+ val settingsHeight by setting("Settings Height", 16.0, 10.0..25.0, 0.1)
+ val padding by setting("Padding", 1.0, 1.0..6.0, 0.1)
+ val listStep by setting("List Step", 1.0, 0.0..6.0, 0.1)
+ val autoResize by setting("Auto Resize", false)
- // Animation
- val openSpeed by setting("Open Speed", 0.5, 0.1..1.0, 0.01)
- val closeSpeed by setting("Close Speed", 0.5, 0.1..1.0, 0.01)
- val scrollSpeed by setting("Scroll Speed", 1.0, 0.1..10.0, 0.01)
+ val roundRadius by setting("Round Radius", 3.0, 0.0..10.0, 0.1)
- // Alignment
- val allowHAlign by setting("Allow H Docking", false)
- val allowVAlign by setting("Allow V Docking", true)
- val dockingGridSize by setting("Docking Grid Size", 1.0, 0.0..20.0, 0.5)
+ val backgroundTint by setting("Background Tint", Color.BLACK.setAlpha(0.4))
- init {
- onEnable {
- LambdaClickGui.show()
+ val titleBackgroundColor by setting("Title Background Color", Color(80, 80, 80))
+ val backgroundColor by setting("Background Color", titleBackgroundColor)
+ val backgroundShade by setting("Background Shade", true)
+
+ val outline by setting("Outline", true)
+ val outlineWidth by setting("Outline Width", 6.0, 1.0..10.0, 0.1) { outline }
+ val outlineColor by setting("Outline Color", Color.WHITE.setAlpha(0.6)) { outline }
+ val outlineShade by setting("Outline Shade", true) { outline }
+ val fontScale by setting("Font Scale", 1.0, 0.5..2.0, 0.1)
+ val fontOffset by setting("Font Offset", 4.0, 0.0..5.0, 0.1)
+ val dockingGridSize by setting("Docking Grid Size", 1.0, 0.1..10.0, 0.1)
+
+ val moduleEnabledColor by setting("Module Enabled Color", Color.WHITE.setAlpha(0.4))
+ val moduleDisabledColor by setting("Module Disabled Color", Color.WHITE.setAlpha(0.0))
+ val moduleHoverAccent by setting("Module Hover Accent", 0.15, 0.0..0.3, 0.01)
+ val moduleOpenAccent by setting("Module Open Accent", 0.3, 0.0..0.5, 0.01)
+
+ val multipleSettingWindows by setting("Multiple Setting Windows", false)
+ val animationCurve by setting("List Animation Curve", AnimationCurve.Normal)
+ val smoothness by setting("Smoothness", 0.4, 0.3..0.7, 0.01) { animationCurve != AnimationCurve.Static }
+
+ val SCREEN get() = gui("Click Gui") {
+ onKeyPress {
+ if (it.keyCode != keybind.keyCode || keybind == KeyCode.UNBOUND) return@onKeyPress
+ mc.currentScreen?.close()
}
- onDisable {
- LambdaClickGui.close()
- LambdaHudGui.close()
+ rect {
+ onUpdate {
+ rect = owner!!.rect
+ setColor(backgroundTint)
+ }
}
- listen(priority = Int.MAX_VALUE) { event ->
- if (mc.options.commandKey.isPressed) return@listen
- if (keybind == KeyCode.UNBOUND) return@listen
- if (event.translated != keybind) return@listen
- // ToDo: Exception for ui text input
- toggle()
+ var x = 10.0
+ val y = x
+
+ ModuleTag.defaults.forEach { tag ->
+ x += moduleWindow(tag, Vec2d(x, y)).width + 5
}
+ }
+
+ enum class AnimationCurve {
+ Normal,
+ Static,
+ Reverse
+ }
- listenUnsafe {
- disable()
+ init {
+ onEnable {
+ SCREEN.show()
+ toggle()
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt
index 17e26f3cd..52055ab17 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/client/GuiSettings.kt
@@ -22,7 +22,6 @@ import com.lambda.event.events.TickEvent
import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe
import com.lambda.graphics.animation.Animation.Companion.exp
import com.lambda.graphics.animation.AnimationTicker
-import com.lambda.gui.impl.clickgui.LambdaClickGui
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import java.awt.Color
@@ -44,9 +43,9 @@ object GuiSettings : Module(
val backgroundColor by setting("Background Color", Color(50, 50, 50, 150), visibility = { page == Page.Colors })
val shade by setting("Shade", true, visibility = { page == Page.Colors })
val shadeBackground by setting("Shade Background", true, visibility = { page == Page.Colors })
- val colorWidth by setting("Shade Width", 400.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors })
- val colorHeight by setting("Shade Height", 400.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors })
- val colorSpeed by setting("Color Speed", 1.0, 0.1..10.0, 0.1, visibility = { page == Page.Colors })
+ val colorWidth by setting("Shade Width", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors })
+ val colorHeight by setting("Shade Height", 200.0, 10.0..1000.0, 10.0, visibility = { page == Page.Colors })
+ val colorSpeed by setting("Color Speed", 1.0, 0.1..5.0, 0.1, visibility = { page == Page.Colors })
val mainColor: Color get() = if (shade) Color.WHITE else primaryColor
@@ -60,7 +59,7 @@ object GuiSettings : Module(
private var targetScale = 2.0
get() {
- val update = System.currentTimeMillis() - lastChange > 200 || !LambdaClickGui.isOpen
+ val update = System.currentTimeMillis() - lastChange > 200 || !ClickGui.SCREEN.isOpen
if (update) field = scaleSetting / 100.0 * 2.0
return field
}
@@ -70,7 +69,7 @@ object GuiSettings : Module(
tick()
}
- exp({ targetScale }, 0.5).apply {
+ exp(0.5) { targetScale }.apply {
listenUnsafe(alwaysListen = true) {
setValue(targetScale)
}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt
index 23d5c8212..b97bca99b 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/client/LambdaMoji.kt
@@ -17,49 +17,86 @@
package com.lambda.module.modules.client
+import com.lambda.Lambda.mc
import com.lambda.event.events.RenderEvent
-import com.lambda.event.events.TickEvent
import com.lambda.event.listener.SafeListener.Companion.listen
-import com.lambda.gui.api.RenderLayer
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawGlyph
+import com.lambda.graphics.renderer.gui.font.core.GlyphInfo
+import com.lambda.graphics.renderer.gui.font.core.LambdaAtlas.get
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.util.math.Vec2d
+import net.minecraft.text.OrderedText
+import net.minecraft.text.Style
+import java.awt.Color
+// This is the worst code I have ever wrote in my life
object LambdaMoji : Module(
name = "LambdaMoji",
description = "",
defaultTags = setOf(ModuleTag.CLIENT, ModuleTag.RENDER),
enabledByDefault = true,
) {
- private val scale by setting("Emoji Scale", 1.0, 0.5..2.0, 0.1)
+ val scale by setting("Emoji Scale", 1.0, 0.5..1.5, 0.1)
+ val suggestions by setting("Chat Suggestions", true)
- private val renderer = RenderLayer()
- private val renderQueue = hashMapOf, List>()
+ private val emojiWhitespace: String
+ get() = " ".repeat(((mc.textRenderer.fontHeight / 2 / mc.textRenderer.getWidth(" ")) * scale).toInt())
+
+ private val renderQueue = mutableListOf>()
init {
- listen {
- var index = 0
- renderQueue.forEach { (emojis, positions) ->
- emojis.forEachIndexed { emojiIndex, emoji ->
- val pos = positions[emojiIndex]
-
- renderer.font.build(
- text = emoji,
- position = Vec2d(pos.x, pos.y * (index.toDouble() + 1)),
- scale = scale,
- )
+ listen {
+ renderQueue.forEach { (glyph, position, color) ->
+ drawGlyph(glyph, position, color)
+ }
+
+ renderQueue.clear()
+ }
+ }
+
+ // FixMe: Doesn't render properly when the chat scale is modified
+ fun parse(text: OrderedText, x: Float, y: Float, color: Int): OrderedText {
+ val saved = mutableMapOf()
+ val builder = StringBuilder()
+
+ var absoluteIndex = 0
+ text.accept { _, style, codePoint ->
+ saved[absoluteIndex++] = style
+ builder.appendCodePoint(codePoint)
+ true
+ }
+
+ var raw = builder.toString()
+ RenderSettings.emojiFont.parse(raw)
+ .forEach { emoji ->
+ val index = raw.indexOf(emoji)
+ if (index == -1) return@forEach
+
+ val height = mc.textRenderer.fontHeight
+ val width = mc.textRenderer.getWidth(raw.substring(0, index))
+
+ // Dude I'm sick of working with the shitcode that is minecraft's codebase :sob:
+ val trueColor = when (color) {
+ 0x00E0E0E0, 0 -> Color(255, 255, 255, 255)
+ else -> Color(255, 255, 255, (color shr 24 and 0xFF))
}
- index++
+ val glyph = RenderSettings.emojiFont[emoji]!!
+ renderQueue.add(Triple(glyph, Vec2d(x + width, y + height / 2), trueColor))
+
+ // Replace the emoji with whitespaces depending on the player's settings
+ raw = raw.replaceFirst(emoji, emojiWhitespace)
}
- }
- listen {
- renderer.render()
+ val constructed = mutableListOf()
+
+ // Will not work properly if the emoji is part of the style
+ saved.forEach { (charIndex: Int, style: Style) ->
+ if (charIndex >= raw.length) return@forEach
+ constructed.add(OrderedText.styledForwardsVisitedString(raw.substring(charIndex, charIndex + 1), style))
}
- }
- fun add(emojis: List, positions: List) {
- renderQueue[emojis] = positions
+ return OrderedText.concat(constructed)
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt
index d5272a72d..08936c229 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/client/RenderSettings.kt
@@ -17,6 +17,8 @@
package com.lambda.module.modules.client
+import com.lambda.graphics.renderer.gui.font.core.LambdaEmoji
+import com.lambda.graphics.renderer.gui.font.core.LambdaFont
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import java.awt.Color
@@ -29,22 +31,23 @@ object RenderSettings : Module(
private val page by setting("Page", Page.Font)
// Font
+ val textFont by setting("Text Font", LambdaFont.FiraSansRegular) { page == Page.Font }
+ val emojiFont by setting("Emoji Font", LambdaEmoji.Twemoji) { page == Page.Font }
val shadow by setting("Shadow", true) { page == Page.Font }
val shadowBrightness by setting("Shadow Brightness", 0.35, 0.0..0.5, 0.01) { page == Page.Font && shadow }
val shadowShift by setting("Shadow Shift", 1.0, 0.0..2.0, 0.05) { page == Page.Font && shadow }
val gap by setting("Gap", 1.5, -10.0..10.0, 0.5) { page == Page.Font }
val baselineOffset by setting("Vertical Offset", 0.0, -10.0..10.0, 0.5) { page == Page.Font }
- private val lodBiasSetting by setting("Smoothing", 0.0, -10.0..10.0, 0.5) { page == Page.Font }
val highlightColor by setting("Text Highlight Color", Color(214, 55, 87), visibility = { page == Page.Font })
+ val sdfMin by setting("SDF Min", 0.4, 0.0..1.0, 0.01, visibility = { page == Page.Font })
+ val sdfMax by setting("SDF Max", 1.0, 0.0..1.0, 0.01, visibility = { page == Page.Font })
// ESP
- val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunk/tick") { page == Page.ESP }
- val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunk/tick") { page == Page.ESP }
+ val uploadsPerTick by setting("Uploads", 16, 1..256, 1, unit = " chunks/tick") { page == Page.ESP }
+ val rebuildsPerTick by setting("Rebuilds", 64, 1..256, 1, unit = " chunks/tick") { page == Page.ESP }
val updateFrequency by setting("Update Frequency", 2, 1..10, 1, "Frequency of block updates", unit = " ticks") { page == Page.ESP }
val outlineWidth by setting("Outline Width", 1.0, 0.1..5.0, 0.1, "Width of block outlines", unit = "px") { page == Page.ESP }
- val lodBias get() = lodBiasSetting * 0.25f - 0.75f
-
private enum class Page {
Font,
ESP,
diff --git a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
index ca4625a73..466fff405 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt
@@ -17,6 +17,8 @@
package com.lambda.module.modules.combat
+import com.lambda.Lambda
+import com.lambda.Lambda.mc
import com.lambda.config.groups.RotationSettings
import com.lambda.config.groups.Targeting
import com.lambda.context.SafeContext
@@ -28,8 +30,7 @@ import com.lambda.graphics.gl.Matrices
import com.lambda.graphics.gl.Matrices.buildWorldProjection
import com.lambda.graphics.gl.Matrices.withVertexTransform
import com.lambda.graphics.renderer.gui.font.FontRenderer
-import com.lambda.graphics.renderer.gui.font.LambdaEmoji
-import com.lambda.graphics.renderer.gui.font.LambdaFont
+import com.lambda.graphics.renderer.gui.font.FontRenderer.drawString
import com.lambda.interaction.request.rotation.Rotation.Companion.rotationTo
import com.lambda.interaction.request.rotation.RotationManager
import com.lambda.interaction.request.rotation.visibilty.VisibilityChecker.getVisibleSurfaces
@@ -130,8 +131,6 @@ object CrystalAura : Module(
}
}
- private val font = FontRenderer(LambdaFont.FiraSansRegular, LambdaEmoji.Twemoji)
-
init {
// Async ticking
fixedRateTimer(
@@ -179,16 +178,13 @@ object CrystalAura : Module(
listen {
if (!debug) return@listen
- // Build the buffer
- blueprint.values.forEach {
- it.buildDebug()
- }
-
- // Draw the font
Matrices.push {
- val c = mc.gameRenderer.camera.pos.negate()
+ val c = Lambda.mc.gameRenderer.camera.pos.negate()
translate(c.x, c.y, c.z)
- font.render()
+ // Build the buffer
+ blueprint.values.forEach {
+ it.buildDebug()
+ }
}
}
@@ -513,11 +509,11 @@ object CrystalAura : Module(
"Self Damage: ${self.roundToStep(0.01)}"
)
- var height = -0.5 * lines.size * (font.getHeight() + 2)
+ var height = -0.5 * lines.size * (FontRenderer.getHeight() + 2)
lines.forEach {
- font.build(it, Vec2d(-font.getWidth(it) * 0.5, height))
- height += font.getHeight() + 2
+ drawString(it, Vec2d(-FontRenderer.getWidth(it) * 0.5, height))
+ height += FontRenderer.getHeight() + 2
}
}
}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt b/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt
index b7786e85e..dad299d2e 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/debug/SettingTest.kt
@@ -19,6 +19,7 @@ package com.lambda.module.modules.debug
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
+import com.lambda.util.Communication.info
import com.lambda.util.KeyCode
import net.minecraft.block.Blocks
import net.minecraft.util.math.BlockPos
@@ -61,9 +62,12 @@ object SettingTest : Module(
private val colorMap by setting("Color Map", mapOf("Primary" to Color.GREEN))
private val keyBindSet by setting("Key Bind Set", setOf(KeyCode.T))
+ // Other
+ private val unitSetting by setting("Unit Test", { this@SettingTest.info("Unit setting") })
+
enum class ExampleEnum {
VALUE_ONE,
VALUE_TWO,
VALUE_THREE
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt
new file mode 100644
index 000000000..74a5da185
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/module/modules/player/MapDownloader.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.player
+
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.util.FolderRegister
+import com.lambda.util.FolderRegister.locationBoundDirectory
+import com.lambda.util.StringUtils.hash
+import com.lambda.util.player.SlotUtils.combined
+import com.lambda.util.world.entitySearch
+import net.minecraft.block.MapColor
+import net.minecraft.entity.decoration.ItemFrameEntity
+import net.minecraft.item.FilledMapItem
+import net.minecraft.item.map.MapState
+import java.awt.image.BufferedImage
+import javax.imageio.ImageIO
+
+object MapDownloader : Module(
+ name = "MapDownloader",
+ description = "Save map data to your computer",
+ defaultTags = setOf(ModuleTag.PLAYER),
+) {
+ init {
+ listen {
+ val mapStates = entitySearch(128.0)
+ .mapNotNull { FilledMapItem.getMapState(it.heldItemStack, world) } +
+ player.combined.mapNotNull { FilledMapItem.getMapState(it, world) }
+
+ mapStates.forEach { map ->
+ val name = map.hash
+ val image = map.toBufferedImage()
+
+ val file = FolderRegister.maps.toFile().locationBoundDirectory().resolve("$name.png")
+ if (file.exists()) return@listen
+
+ ImageIO.write(image, "png", file)
+ }
+ }
+ }
+
+ private val MapState.hash: String
+ get() = colors.hash("SHA-256")
+
+ fun MapState.toBufferedImage(): BufferedImage {
+ val image = BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB)
+
+ repeat(128) { x ->
+ repeat(128) { y ->
+ val index = colors[x + y * 128].toInt()
+ val color = MapColor.getRenderColor(index)
+
+ val b = (color shr 16) and 0xFF
+ val g = (color shr 8) and 0xFF
+ val r = (color shr 0) and 0xFF
+
+ val argb = -0x1000000 or (r shl 16) or (g shl 8) or (b shl 0)
+ image.setRGB(x, y, argb)
+ }
+ }
+
+ return image
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt
new file mode 100644
index 000000000..e299d1ff5
--- /dev/null
+++ b/common/src/main/kotlin/com/lambda/module/modules/render/MapPreview.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2025 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.render
+
+import com.lambda.Lambda.mc
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.mojang.blaze3d.systems.RenderSystem
+import net.minecraft.client.font.TextRenderer
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.tooltip.TooltipComponent
+import net.minecraft.item.FilledMapItem
+import net.minecraft.item.ItemStack
+import net.minecraft.item.map.MapState
+import net.minecraft.util.Identifier
+
+
+object MapPreview : Module(
+ name = "MapPreview",
+ description = "Preview maps in your inventory",
+ defaultTags = setOf(ModuleTag.RENDER)
+) {
+ private val background = Identifier("textures/map/map_background.png")
+
+ // The map component is added via the draw context mixin, thanks mojang
+ class MapComponent(val stack: ItemStack) : TooltipComponent {
+ val state: MapState?
+ get() = FilledMapItem.getMapState(stack, mc.world)
+
+ val mapId: Int?
+ get() = FilledMapItem.getMapId(stack)
+
+ override fun drawItems(fontRenderer: TextRenderer, x: Int, y: Int, context: DrawContext) {
+ mapId?.let { id ->
+ // Values taken from net.minecraft.client.render.item.HeldItemRenderer.renderFirstPersonMap
+
+ val matrices = context.matrices
+
+ matrices.push()
+ matrices.translate(x + 4.0, y + 4.0, 500.0)
+ matrices.scale(0.7f, 0.7f, 1f)
+
+ RenderSystem.enableBlend()
+ context.drawTexture(background, -7, -7, 0f, 0f, 142, 142, 142, 142)
+
+ matrices.translate(0.0, 0.0, 1.0)
+ mc.gameRenderer.mapRenderer.draw(matrices, context.vertexConsumers, id, state, true, 240)
+ }
+ }
+
+ override fun getHeight(): Int {
+ return if (FilledMapItem.getMapState(stack, mc.world) != null) 100
+ else 0
+ }
+
+ override fun getWidth(textRenderer: TextRenderer) = 72
+ }
+}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt b/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
index 35c810b4f..b1a38d5c5 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/render/NoRender.kt
@@ -39,4 +39,7 @@ object NoRender : Module(
@JvmStatic
val noInWall by setting("No In Wall Overlay", true)
+
+ @JvmStatic
+ val noChatVerificationToast by setting("No Chat Verification Toast", true)
}
diff --git a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt
index 54c60613e..d77819dbd 100644
--- a/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt
+++ b/common/src/main/kotlin/com/lambda/module/modules/render/Particles.kt
@@ -32,7 +32,7 @@ import com.lambda.graphics.gl.GlStateUtils.withDepth
import com.lambda.graphics.gl.Matrices
import com.lambda.graphics.gl.Matrices.buildWorldProjection
import com.lambda.graphics.gl.Matrices.withVertexTransform
-import com.lambda.graphics.shader.Shader
+import com.lambda.graphics.shader.Shader.Companion.shader
import com.lambda.interaction.request.rotation.Rotation
import com.lambda.module.Module
import com.lambda.module.modules.client.GuiSettings
@@ -81,7 +81,7 @@ object Particles : Module(
private var particles = mutableListOf()
private val pipeline = VertexPipeline(VertexMode.TRIANGLES, VertexAttrib.Group.PARTICLE)
- private val shader = Shader("renderer/particle", "renderer/particle")
+ private val shader = shader("renderer/particle", "renderer/particle")
init {
listen {
@@ -98,7 +98,7 @@ object Particles : Module(
shader["u_CameraPosition"] = mc.gameRenderer.camera.pos
pipeline.upload()
- withDepth(pipeline::render)
+ withDepth(false, pipeline::render)
pipeline.clear()
}
}
diff --git a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt
index b436dff5d..28348f60a 100644
--- a/common/src/main/kotlin/com/lambda/util/FolderRegister.kt
+++ b/common/src/main/kotlin/com/lambda/util/FolderRegister.kt
@@ -48,9 +48,10 @@ object FolderRegister : Loadable {
val replay: Path = lambda.resolve("replay")
val cache: Path = lambda.resolve("cache")
val structure: Path = lambda.resolve("structure")
+ val maps: Path = lambda.resolve("maps")
override fun load(): String {
- val folders = listOf(lambda, config, packetLogs, replay, cache, structure)
+ val folders = listOf(lambda, config, packetLogs, replay, cache, structure, maps)
val createdFolders = folders.mapNotNull {
if (it.notExists()) {
it.createDirectories()
diff --git a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt
index 22dae8ec1..e1e39688d 100644
--- a/common/src/main/kotlin/com/lambda/util/LambdaResource.kt
+++ b/common/src/main/kotlin/com/lambda/util/LambdaResource.kt
@@ -17,9 +17,21 @@
package com.lambda.util
+import com.lambda.Lambda
+import java.awt.image.BufferedImage
+import java.io.FileNotFoundException
import java.io.InputStream
+import java.net.URL
+import javax.imageio.ImageIO
-class LambdaResource(val path: String) {
- val stream: InputStream?
- get() = javaClass.getResourceAsStream("/assets/lambda/$path")
-}
+typealias LambdaResource = String
+
+val LambdaResource.stream: InputStream
+ get() = Lambda::class.java.getResourceAsStream("/assets/lambda/$this")
+ ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found")
+
+val LambdaResource.url: URL
+ get() = Lambda::class.java.getResource("/assets/lambda/$this")
+ ?: throw FileNotFoundException("File \"/assets/lambda/$this\" not found")
+
+fun LambdaResource.readImage(): BufferedImage = ImageIO.read(this.stream)
diff --git a/common/src/main/kotlin/com/lambda/util/Mouse.kt b/common/src/main/kotlin/com/lambda/util/Mouse.kt
index 8e92b81ab..a215325a8 100644
--- a/common/src/main/kotlin/com/lambda/util/Mouse.kt
+++ b/common/src/main/kotlin/com/lambda/util/Mouse.kt
@@ -17,7 +17,17 @@
package com.lambda.util
+import com.lambda.Lambda.mc
+import com.mojang.blaze3d.systems.RenderSystem
import org.lwjgl.glfw.GLFW
+import org.lwjgl.glfw.GLFW.GLFW_ARROW_CURSOR
+import org.lwjgl.glfw.GLFW.GLFW_POINTING_HAND_CURSOR
+import org.lwjgl.glfw.GLFW.GLFW_RESIZE_EW_CURSOR
+import org.lwjgl.glfw.GLFW.GLFW_RESIZE_NS_CURSOR
+import org.lwjgl.glfw.GLFW.GLFW_RESIZE_NWSE_CURSOR
+import org.lwjgl.glfw.GLFW.glfwCreateStandardCursor
+import org.lwjgl.glfw.GLFW.glfwSetCursor
+import kotlin.jvm.Throws
class Mouse {
enum class Button(val key: Int) {
@@ -63,4 +73,42 @@ class Mouse {
nameMap[name.lowercase()] ?: throw IllegalArgumentException("Action name '$name' not found in nameMap.")
}
}
+
+ enum class Cursor(private val getCursorPointer: () -> Long) {
+ Arrow(::arrow),
+ Pointer(::pointer),
+ ResizeH(::resizeH), ResizeV(::resizeV), ResizeHV(::resizeHV);
+
+ fun set() {
+ if (lastCursor == this) return
+ lastCursor = this
+
+ RenderSystem.assertOnRenderThread()
+ glfwSetCursor(mc.window.handle, getCursorPointer())
+ }
+ }
+
+ // ToDo: replace by event
+ class CursorController {
+ private var lastSetCursor: Cursor? = null
+
+ fun setCursor(cursor: Cursor) {
+ // We're doing this to let other controllers be able to set the cursor when this one doesn't change
+ if (lastSetCursor == cursor && cursor == Cursor.Arrow) return
+
+ cursor.set()
+ lastSetCursor = cursor
+ }
+
+ fun reset() = setCursor(Cursor.Arrow)
+ }
+
+ companion object {
+ private val arrow by lazy { glfwCreateStandardCursor(GLFW_ARROW_CURSOR) }
+ private val pointer by lazy { glfwCreateStandardCursor(GLFW_POINTING_HAND_CURSOR) }
+ private val resizeH by lazy { glfwCreateStandardCursor(GLFW_RESIZE_EW_CURSOR) }
+ private val resizeV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NS_CURSOR) }
+ private val resizeHV by lazy { glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR) }
+ var lastCursor = Cursor.Arrow
+ }
}
diff --git a/common/src/main/kotlin/com/lambda/util/StringUtils.kt b/common/src/main/kotlin/com/lambda/util/StringUtils.kt
index ef1ce4fdf..28eaa1894 100644
--- a/common/src/main/kotlin/com/lambda/util/StringUtils.kt
+++ b/common/src/main/kotlin/com/lambda/util/StringUtils.kt
@@ -17,6 +17,8 @@
package com.lambda.util
+import java.security.MessageDigest
+
object StringUtils {
/**
* Returns a sanitized file path for both Unix and Linux systems
@@ -89,4 +91,32 @@ object StringUtils {
return cost[len0 - 1]
}
+
+ /**
+ * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification
+ *
+ * @receiver The string to hash
+ * @param algorithm The algorithm instance to use
+ *
+ * @return The string representation of the hash
+ */
+ fun String.hash(algorithm: String): String =
+ MessageDigest
+ .getInstance(algorithm)
+ .digest(toByteArray())
+ .joinToString(separator = "") { "%02x".format(it) }
+
+ /**
+ * See [MessageDigest section](https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#messagedigest-algorithms) of the Java Security Standard Algorithm Names Specification
+ *
+ * @receiver The byte array to hash
+ * @param algorithm The algorithm instance to use
+ *
+ * @return The string representation of the hash
+ */
+ fun ByteArray.hash(algorithm: String): String =
+ MessageDigest
+ .getInstance(algorithm)
+ .digest(this)
+ .joinToString(separator = "") { "%02x".format(it) }
}
diff --git a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt
index 8d9ac1355..ece4115a0 100644
--- a/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt
+++ b/common/src/main/kotlin/com/lambda/util/math/MathUtils.kt
@@ -41,22 +41,17 @@ object MathUtils {
fun Int.logCap(minimum: Int) = max(minimum.toDouble(), ceil(log2(toDouble()))).toInt()
fun T.roundToStep(step: T): T {
- val stepD = step.toDouble()
- if (stepD == 0.0) return this
-
- var value = round(toDouble() / stepD) * stepD
- value = value.roundToPlaces(stepD.decimals)
- if (abs(value) == 0.0) value = 0.0
-
- return typeConvert(value)
+ val valueBD = BigDecimal(toString())
+ val stepBD = BigDecimal(step.toString())
+ if (stepBD.compareTo(BigDecimal.ZERO) == 0) return this
+ val scaled = valueBD.divide(stepBD, stepBD.scale(), RoundingMode.HALF_UP)
+ .setScale(0, RoundingMode.HALF_UP)
+ .multiply(stepBD)
+ .setScale(stepBD.scale(), RoundingMode.HALF_UP)
+
+ return typeConvert(scaled.toDouble())
}
- private fun Double.roundToPlaces(places: Int) =
- BigDecimal(this).setScale(places, RoundingMode.HALF_EVEN).toDouble()
-
- private val Double.decimals: Int
- get() = BigDecimal.valueOf(this).scale()
-
fun T.typeConvert(valueIn: Double): T {
@Suppress("UNCHECKED_CAST")
return when (this) {
diff --git a/common/src/main/kotlin/com/lambda/util/math/Rect.kt b/common/src/main/kotlin/com/lambda/util/math/Rect.kt
index bc94d0559..73c902382 100644
--- a/common/src/main/kotlin/com/lambda/util/math/Rect.kt
+++ b/common/src/main/kotlin/com/lambda/util/math/Rect.kt
@@ -21,10 +21,10 @@ import kotlin.math.max
import kotlin.math.min
data class Rect(private val pos1: Vec2d, private val pos2: Vec2d) {
- val left = pos1.x
- val top = pos1.y
- val right = pos2.x
- val bottom = pos2.y
+ val left get() = pos1.x
+ val top get() = pos1.y
+ val right get() = pos2.x
+ val bottom get() = pos2.y
val leftTop get() = Vec2d(left, top)
val rightTop get() = Vec2d(right, top)
@@ -65,6 +65,9 @@ data class Rect(private val pos1: Vec2d, private val pos2: Vec2d) {
fun basedOn(base: Vec2d, width: Double, height: Double) =
Rect(base, base + Vec2d(width, height))
+ fun basedOn(base: Vec2d, width: Int, height: Int) =
+ Rect(base, base + Vec2d(width, height))
+
fun basedOn(base: Vec2d, size: Vec2d) =
Rect(base, base + size)
diff --git a/common/src/main/resources/assets/lambda/chika.gif b/common/src/main/resources/assets/lambda/chika.gif
new file mode 100644
index 000000000..2def218ec
Binary files /dev/null and b/common/src/main/resources/assets/lambda/chika.gif differ
diff --git a/common/src/main/resources/assets/lambda/fonts/emojis.zip b/common/src/main/resources/assets/lambda/fonts/emojis.zip
new file mode 100644
index 000000000..5ca722f52
Binary files /dev/null and b/common/src/main/resources/assets/lambda/fonts/emojis.zip differ
diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag
new file mode 100644
index 000000000..37aa5a897
--- /dev/null
+++ b/common/src/main/resources/assets/lambda/shaders/fragment/font/font.frag
@@ -0,0 +1,27 @@
+#version 330 core
+
+uniform sampler2D u_FontTexture;
+uniform sampler2D u_EmojiTexture;
+uniform float u_SDFMin;
+uniform float u_SDFMax;
+
+in vec2 v_TexCoord;
+in vec4 v_Color;
+
+out vec4 color;
+
+float sdf(float channel) {
+ return 1.0 - smoothstep(u_SDFMin, u_SDFMax, 1.0 - channel);
+}
+
+void main() {
+ bool isEmoji = v_TexCoord.x < 0.0;
+
+ if (isEmoji) {
+ vec4 c = texture(u_EmojiTexture, -v_TexCoord);
+ color = vec4(c.rgb, sdf(c.a)) * v_Color;
+ return;
+ }
+
+ color = vec4(1.0, 1.0, 1.0, sdf(texture(u_FontTexture, v_TexCoord).r)) * v_Color;
+}
diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag
deleted file mode 100644
index 25cd3d0f7..000000000
--- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/font.frag
+++ /dev/null
@@ -1,21 +0,0 @@
-#version 330 core
-
-uniform sampler2D u_FontTexture;
-uniform sampler2D u_EmojiTexture;
-
-in vec2 v_TexCoord;
-in vec4 v_Color;
-
-out vec4 color;
-
-void main() {
- vec4 tex;
-
- if (v_TexCoord.x > 0.0) {
- tex = texture(u_FontTexture, v_TexCoord);
- } else {
- tex = texture(u_EmojiTexture, -v_TexCoord);
- }
-
- color = tex * v_Color;
-}
diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag
index 306a00247..97c5b5d34 100644
--- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag
+++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_filled.frag
@@ -1,21 +1,25 @@
#version 330 core
-uniform float u_Time;
-uniform vec4 u_Color1;
-uniform vec4 u_Color2;
uniform vec2 u_Size;
+uniform float u_RoundLeftTop;
+uniform float u_RoundLeftBottom;
+uniform float u_RoundRightBottom;
+uniform float u_RoundRightTop;
+
+uniform float u_Shade;
+uniform float u_ShadeTime;
+uniform vec4 u_ShadeColor1;
+uniform vec4 u_ShadeColor2;
+uniform vec2 u_ShadeSize;
in vec2 v_Position;
in vec2 v_TexCoord;
in vec4 v_Color;
-in vec2 v_Size;
-in float v_RoundRadius;
-in float v_Shade;
out vec4 color;
#define SMOOTHING 0.25
-#define NOISE_GRANULARITY 0.005
+#define NOISE_GRANULARITY 0.004
vec4 noise() {
// https://shader-tutorial.dev/advanced/color-banding-dithering/
@@ -25,21 +29,45 @@ vec4 noise() {
}
vec4 shade() {
- if (v_Shade != 1.0) return v_Color;
+ if (u_Shade != 1.0) return v_Color;
+
+ vec2 pos = v_Position * u_ShadeSize;
+ float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5;
+
+ return mix(u_ShadeColor1, u_ShadeColor2, p) * v_Color;
+}
+
+float getRoundRadius() {
+ // ToDo: use step
+ bool xcmp = v_TexCoord.x > 0.5;
+ bool ycmp = v_TexCoord.y > 0.5;
+
+ float r = 0.0;
- vec2 pos = v_Position * u_Size;
- float p = sin(pos.x - pos.y - u_Time) * 0.5 + 0.5;
+ if (xcmp) {
+ if (ycmp) {
+ r = u_RoundRightBottom;
+ } else {
+ r = u_RoundRightTop;
+ }
+ } else {
+ if (ycmp) {
+ r = u_RoundLeftBottom;
+ } else {
+ r = u_RoundLeftTop;
+ }
+ }
- return mix(u_Color1, u_Color2, p) * v_Color;
+ return r;
}
vec4 round() {
- vec2 halfSize = v_Size * 0.5;
+ vec2 halfSize = u_Size * 0.5;
- float radius = max(v_RoundRadius, SMOOTHING);
+ float radius = max(getRoundRadius(), SMOOTHING);
vec2 smoothVec = vec2(SMOOTHING);
- vec2 coord = mix(-smoothVec, v_Size + smoothVec, v_TexCoord);
+ vec2 coord = mix(-smoothVec, u_Size + smoothVec, v_TexCoord);
vec2 center = halfSize - coord;
float distance = length(max(abs(center) - halfSize + radius, 0.0)) - radius;
diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag
index de9501215..71e8680fe 100644
--- a/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag
+++ b/common/src/main/resources/assets/lambda/shaders/fragment/renderer/rect_outline.frag
@@ -1,24 +1,24 @@
#version 330 core
-uniform float u_Time;
-uniform vec4 u_Color1;
-uniform vec4 u_Color2;
-uniform vec2 u_Size;
+uniform float u_Shade;
+uniform float u_ShadeTime;
+uniform vec4 u_ShadeColor1;
+uniform vec4 u_ShadeColor2;
+uniform vec2 u_ShadeSize;
in vec2 v_Position;
in float v_Alpha;
in vec4 v_Color;
-in float v_Shade;
out vec4 color;
vec4 shade() {
- if (v_Shade != 1.0) return v_Color;
+ if (u_Shade != 1.0) return v_Color;
- vec2 pos = v_Position * u_Size;
- float p = sin(pos.x - pos.y - u_Time) * 0.5 + 0.5;
+ vec2 pos = v_Position * u_ShadeSize;
+ float p = sin(pos.x - pos.y - u_ShadeTime) * 0.5 + 0.5;
- return mix(u_Color1, u_Color2, p) * v_Color;
+ return mix(u_ShadeColor1, u_ShadeColor2, p) * v_Color;
}
vec4 glow() {
diff --git a/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag
new file mode 100644
index 000000000..5d4f485f1
--- /dev/null
+++ b/common/src/main/resources/assets/lambda/shaders/fragment/signed_distance_field.frag
@@ -0,0 +1,28 @@
+#version 330 core
+
+uniform sampler2D u_Texture;
+uniform vec2 u_TexelSize;
+
+in vec2 v_TexCoord;
+out vec4 color;
+
+#define SPHREAD 4
+
+void main() {
+ vec4 colors = vec4(0.0);
+ vec4 blurWeight = vec4(0.0);
+
+ for (int x = -SPHREAD; x <= SPHREAD; ++x) {
+ for (int y = -SPHREAD; y <= SPHREAD; ++y) {
+ vec2 offset = vec2(x, y) * u_TexelSize;
+
+ vec4 color = texture(u_Texture, v_TexCoord + offset);
+ vec4 weight = exp(-color * color);
+
+ colors += color * weight;
+ blurWeight += weight;
+ }
+ }
+
+ color = colors / blurWeight;
+}
diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert b/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert
similarity index 99%
rename from common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert
rename to common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert
index 14d1bb437..5f16359df 100644
--- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/font.vert
+++ b/common/src/main/resources/assets/lambda/shaders/vertex/font/font.vert
@@ -11,7 +11,6 @@ out vec4 v_Color;
void main() {
gl_Position = u_ProjModel * pos;
-
v_TexCoord = uv;
v_Color = color;
}
\ No newline at end of file
diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert
index e52b0bda8..225d9dfd7 100644
--- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert
+++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_filled.vert
@@ -2,19 +2,13 @@
layout (location = 0) in vec4 pos;
layout (location = 1) in vec2 uv;
-layout (location = 2) in vec2 size;
-layout (location = 3) in float round;
-layout (location = 4) in float shade;
-layout (location = 5) in vec4 color;
+layout (location = 2) in vec4 color;
uniform mat4 u_ProjModel;
out vec2 v_Position;
out vec2 v_TexCoord;
out vec4 v_Color;
-out vec2 v_Size;
-out float v_RoundRadius;
-out float v_Shade;
void main() {
gl_Position = u_ProjModel * pos;
@@ -22,8 +16,4 @@ void main() {
v_Position = gl_Position.xy * 0.5 + 0.5;
v_TexCoord = uv;
v_Color = color;
-
- v_Size = size;
- v_RoundRadius = round;
- v_Shade = shade;
}
\ No newline at end of file
diff --git a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert
index 03650690f..42e8d4ac0 100644
--- a/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert
+++ b/common/src/main/resources/assets/lambda/shaders/vertex/renderer/rect_outline.vert
@@ -1,22 +1,22 @@
#version 330 core
layout (location = 0) in vec4 pos;
-layout (location = 1) in float alpha;
-layout (location = 2) in float shade;
+layout (location = 1) in vec2 uv;
+layout (location = 2) in float alpha;
layout (location = 3) in vec4 color;
uniform mat4 u_ProjModel;
out vec2 v_Position;
+out vec2 v_TexCoord;
out float v_Alpha;
out vec4 v_Color;
-out float v_Shade;
void main() {
gl_Position = u_ProjModel * pos;
v_Position = gl_Position.xy * 0.5 + 0.5;
+ v_TexCoord = uv;
v_Alpha = alpha;
v_Color = color;
- v_Shade = shade;
}
\ No newline at end of file
diff --git a/common/src/main/resources/lambda.accesswidener b/common/src/main/resources/lambda.accesswidener
index 43becded7..9e33b952c 100644
--- a/common/src/main/resources/lambda.accesswidener
+++ b/common/src/main/resources/lambda.accesswidener
@@ -28,7 +28,6 @@ accessible field net/minecraft/client/network/AbstractClientPlayerEntity playerL
accessible field net/minecraft/entity/LivingEntity jumpingCooldown I
accessible field net/minecraft/entity/Entity pos Lnet/minecraft/util/math/Vec3d;
accessible field net/minecraft/client/network/ClientPlayerInteractionManager lastSelectedSlot I
-accessible method net/minecraft/entity/Entity isAlwaysInvulnerableTo (Lnet/minecraft/entity/damage/DamageSource;)Z
accessible method net/minecraft/entity/LivingEntity modifyAppliedDamage (Lnet/minecraft/entity/damage/DamageSource;F)F
accessible method net/minecraft/entity/LivingEntity applyArmorToDamage (Lnet/minecraft/entity/damage/DamageSource;F)F
@@ -79,3 +78,4 @@ accessible method net/minecraft/util/math/Vec3i setZ (I)Lnet/minecraft/util/math
accessible field net/minecraft/world/explosion/Explosion behavior Lnet/minecraft/world/explosion/ExplosionBehavior;
accessible field net/minecraft/structure/StructureTemplate blockInfoLists Ljava/util/List;
accessible method net/minecraft/item/BlockItem getPlacementState (Lnet/minecraft/item/ItemPlacementContext;)Lnet/minecraft/block/BlockState;
+accessible field net/minecraft/client/gui/screen/ingame/HandledScreen focusedSlot Lnet/minecraft/screen/slot/Slot;
diff --git a/common/src/main/resources/lambda.mixins.common.json b/common/src/main/resources/lambda.mixins.common.json
index dbc6369e0..b4559b085 100644
--- a/common/src/main/resources/lambda.mixins.common.json
+++ b/common/src/main/resources/lambda.mixins.common.json
@@ -28,9 +28,11 @@
"render.BackgroundRendererMixin",
"render.BlockRenderManagerMixin",
"render.CameraMixin",
+ "render.ChatHudMixin",
"render.ChatInputSuggestorMixin",
"render.ChatScreenMixin",
"render.DebugHudMixin",
+ "render.DrawContextMixin",
"render.GameRendererMixin",
"render.GlStateManagerMixin",
"render.InGameHudMixin",