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",