diff --git a/api/Elementa.api b/api/Elementa.api index 59370b52..1fc09c94 100644 --- a/api/Elementa.api +++ b/api/Elementa.api @@ -160,7 +160,9 @@ public abstract class gg/essential/elementa/UIComponent : java/util/Observable { public final class gg/essential/elementa/UIComponent$Companion { public final fun getDEBUG_OUTLINE_WIDTH ()D public final fun guiHint (DZ)D + public final fun guiHint (DZLgg/essential/elementa/UIComponent;)D public final fun guiHint (FZ)F + public final fun guiHint (FZLgg/essential/elementa/UIComponent;)F } public class gg/essential/elementa/UIConstraints : java/util/Observable { @@ -656,7 +658,7 @@ public class gg/essential/elementa/components/UIShape : gg/essential/elementa/UI public final fun setDrawMode (I)V } -public class gg/essential/elementa/components/UIText : gg/essential/elementa/UIComponent { +public class gg/essential/elementa/components/UIText : gg/essential/elementa/UIComponent, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)V public synthetic fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -680,7 +682,7 @@ public class gg/essential/elementa/components/UIText : gg/essential/elementa/UIC public final fun setText (Ljava/lang/String;)Lgg/essential/elementa/components/UIText; } -public class gg/essential/elementa/components/UIWrappedText : gg/essential/elementa/UIComponent { +public class gg/essential/elementa/components/UIWrappedText : gg/essential/elementa/UIComponent, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (Lgg/essential/elementa/state/State;)V public fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)V @@ -732,7 +734,10 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen public final fun getFocusedComponent ()Lgg/essential/elementa/UIComponent; public fun getHeight ()F public final fun getHoveredFloatingComponent ()Lgg/essential/elementa/UIComponent; + public final fun getKeyboardManager ()Lgg/essential/elementa/manager/KeyboardManager; public fun getLeft ()F + public final fun getMousePositionManager ()Lgg/essential/elementa/manager/MousePositionManager; + public final fun getResolutionManager ()Lgg/essential/elementa/manager/ResolutionManager; public fun getRight ()F public fun getTop ()F public fun getWidth ()F @@ -744,6 +749,9 @@ public final class gg/essential/elementa/components/Window : gg/essential/elemen public fun mouseScroll (D)V public final fun removeFloatingComponent (Lgg/essential/elementa/UIComponent;)V public final fun setHoveredFloatingComponent (Lgg/essential/elementa/UIComponent;)V + public final fun setKeyboardManager (Lgg/essential/elementa/manager/KeyboardManager;)V + public final fun setMousePositionManager (Lgg/essential/elementa/manager/MousePositionManager;)V + public final fun setResolutionManager (Lgg/essential/elementa/manager/ResolutionManager;)V public final fun unfocus ()V } @@ -1127,6 +1135,7 @@ public final class gg/essential/elementa/components/inspector/Inspector : gg/ess public synthetic fun (Lgg/essential/elementa/UIComponent;Ljava/awt/Color;Ljava/awt/Color;FLgg/essential/elementa/constraints/HeightConstraint;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun animationFrame ()V public fun draw (Lgg/essential/universal/UMatrixStack;)V + public final fun setDetached (Z)V } public final class gg/essential/elementa/components/inspector/Inspector$Companion { @@ -1140,6 +1149,11 @@ public final class gg/essential/elementa/components/inspector/InspectorNode : gg public final fun getTargetComponent ()Lgg/essential/elementa/UIComponent; } +public final class gg/essential/elementa/components/inspector/state/StateTextInput : gg/essential/elementa/components/input/v2/UITextInput { + public fun (Lgg/essential/elementa/state/State;ZFLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lgg/essential/elementa/state/State;ZFLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + public final class gg/essential/elementa/components/plot/Bounds { public static final field Companion Lgg/essential/elementa/components/plot/Bounds$Companion; public fun (Ljava/lang/Number;Ljava/lang/Number;IZLjava/awt/Color;Lkotlin/jvm/functions/Function1;)V @@ -1294,7 +1308,7 @@ public final class gg/essential/elementa/constraints/AdditiveConstraint : gg/ess public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/AlphaAspectColorConstraint : gg/essential/elementa/constraints/ColorConstraint { +public final class gg/essential/elementa/constraints/AlphaAspectColorConstraint : gg/essential/elementa/constraints/ColorConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)V public fun (Ljava/awt/Color;F)V @@ -1317,10 +1331,11 @@ public final class gg/essential/elementa/constraints/AlphaAspectColorConstraint public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/AspectConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint { +public final class gg/essential/elementa/constraints/AspectConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (F)V public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/state/State;)V public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; @@ -1384,10 +1399,11 @@ public final class gg/essential/elementa/constraints/ChildBasedRangeConstraint : public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/ChildBasedSizeConstraint : gg/essential/elementa/constraints/SizeConstraint { +public final class gg/essential/elementa/constraints/ChildBasedSizeConstraint : gg/essential/elementa/constraints/SizeConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (F)V public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/state/State;)V public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; @@ -1488,7 +1504,24 @@ public final class gg/essential/elementa/constraints/ColorConstraint$DefaultImpl public static fun visit (Lgg/essential/elementa/constraints/ColorConstraint;Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;Z)V } -public final class gg/essential/elementa/constraints/ConstantColorConstraint : gg/essential/elementa/constraints/ColorConstraint { +public final class gg/essential/elementa/constraints/ColumnPositionConstraint : gg/essential/elementa/constraints/PaddingConstraint, gg/essential/elementa/constraints/XConstraint, gg/essential/elementa/debug/StateRegistry { + public fun (F)V + public fun (Lgg/essential/elementa/state/State;)V + public fun getCachedValue ()Ljava/lang/Float; + public synthetic fun getCachedValue ()Ljava/lang/Object; + public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; + public fun getHorizontalPadding (Lgg/essential/elementa/UIComponent;)F + public fun getRecalculate ()Z + public fun getVerticalPadding (Lgg/essential/elementa/UIComponent;)F + public fun getXPositionImpl (Lgg/essential/elementa/UIComponent;)F + public fun setCachedValue (F)V + public synthetic fun setCachedValue (Ljava/lang/Object;)V + public fun setConstrainTo (Lgg/essential/elementa/UIComponent;)V + public fun setRecalculate (Z)V + public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V +} + +public final class gg/essential/elementa/constraints/ConstantColorConstraint : gg/essential/elementa/constraints/ColorConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (Lgg/essential/elementa/state/State;)V public fun (Ljava/awt/Color;)V @@ -1774,7 +1807,7 @@ public abstract interface class gg/essential/elementa/constraints/PaddingConstra public abstract fun getVerticalPadding (Lgg/essential/elementa/UIComponent;)F } -public final class gg/essential/elementa/constraints/PixelConstraint : gg/essential/elementa/constraints/MasterConstraint { +public final class gg/essential/elementa/constraints/PixelConstraint : gg/essential/elementa/constraints/MasterConstraint, gg/essential/elementa/debug/StateRegistry { public fun (F)V public fun (FZ)V public fun (FZZ)V @@ -1837,10 +1870,12 @@ public final class gg/essential/elementa/constraints/RadiusConstraint$DefaultImp public static fun visit (Lgg/essential/elementa/constraints/RadiusConstraint;Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;Z)V } -public final class gg/essential/elementa/constraints/RainbowColorConstraint : gg/essential/elementa/constraints/ColorConstraint { +public final class gg/essential/elementa/constraints/RainbowColorConstraint : gg/essential/elementa/constraints/ColorConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V + public fun (I)V public fun (IF)V public synthetic fun (IFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)V public fun animationFrame ()V public final fun getAlpha ()I public fun getCachedValue ()Ljava/awt/Color; @@ -1858,7 +1893,7 @@ public final class gg/essential/elementa/constraints/RainbowColorConstraint : gg public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/RelativeConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint { +public final class gg/essential/elementa/constraints/RelativeConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (F)V public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -1882,10 +1917,11 @@ public final class gg/essential/elementa/constraints/RelativeConstraint : gg/ess public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/RelativeWindowConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint { +public final class gg/essential/elementa/constraints/RelativeWindowConstraint : gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/constraints/SizeConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (F)V public synthetic fun (FILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/state/State;)V public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; @@ -1904,10 +1940,11 @@ public final class gg/essential/elementa/constraints/RelativeWindowConstraint : public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/RoundingConstraint : gg/essential/elementa/constraints/MasterConstraint { +public final class gg/essential/elementa/constraints/RoundingConstraint : gg/essential/elementa/constraints/MasterConstraint, gg/essential/elementa/debug/StateRegistry { public fun (Lgg/essential/elementa/constraints/SuperConstraint;)V public fun (Lgg/essential/elementa/constraints/SuperConstraint;Lgg/essential/elementa/constraints/RoundingConstraint$Mode;)V public synthetic fun (Lgg/essential/elementa/constraints/SuperConstraint;Lgg/essential/elementa/constraints/RoundingConstraint$Mode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/constraints/SuperConstraint;Lgg/essential/elementa/state/State;)V public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; @@ -1935,7 +1972,7 @@ public final class gg/essential/elementa/constraints/RoundingConstraint$Mode : j public static fun values ()[Lgg/essential/elementa/constraints/RoundingConstraint$Mode; } -public final class gg/essential/elementa/constraints/ScaleConstraint : gg/essential/elementa/constraints/MasterConstraint { +public final class gg/essential/elementa/constraints/ScaleConstraint : gg/essential/elementa/constraints/MasterConstraint, gg/essential/elementa/debug/StateRegistry { public fun (Lgg/essential/elementa/constraints/SuperConstraint;F)V public fun (Lgg/essential/elementa/constraints/SuperConstraint;Lgg/essential/elementa/state/State;)V public fun animationFrame ()V @@ -1960,8 +1997,9 @@ public final class gg/essential/elementa/constraints/ScaleConstraint : gg/essent public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public final class gg/essential/elementa/constraints/ScaledTextConstraint : gg/essential/elementa/constraints/SizeConstraint { +public final class gg/essential/elementa/constraints/ScaledTextConstraint : gg/essential/elementa/constraints/SizeConstraint, gg/essential/elementa/debug/StateRegistry { public fun (F)V + public fun (Lgg/essential/elementa/state/State;)V public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; public fun getConstrainTo ()Lgg/essential/elementa/UIComponent; @@ -1980,11 +2018,12 @@ public final class gg/essential/elementa/constraints/ScaledTextConstraint : gg/e public fun visitImpl (Lgg/essential/elementa/constraints/resolution/ConstraintVisitor;Lgg/essential/elementa/constraints/ConstraintType;)V } -public class gg/essential/elementa/constraints/SiblingConstraint : gg/essential/elementa/constraints/PaddingConstraint, gg/essential/elementa/constraints/PositionConstraint { +public class gg/essential/elementa/constraints/SiblingConstraint : gg/essential/elementa/constraints/PaddingConstraint, gg/essential/elementa/constraints/PositionConstraint, gg/essential/elementa/debug/StateRegistry { public fun ()V public fun (F)V public fun (FZ)V public synthetic fun (FZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)V public final fun getAlignOpposite ()Z public fun getCachedValue ()Ljava/lang/Float; public synthetic fun getCachedValue ()Ljava/lang/Object; @@ -2909,6 +2948,9 @@ public final class gg/essential/elementa/font/data/PlaneBounds { public fun toString ()Ljava/lang/String; } +public final class gg/essential/elementa/manager/KeyboardManager$DefaultImpls { +} + public final class gg/essential/elementa/markdown/BlockquoteConfig { public fun ()V public fun (F)V @@ -4304,15 +4346,32 @@ public final class gg/essential/elementa/utils/BindingKt { } public final class gg/essential/elementa/utils/ExtensionsKt { + public static final fun and (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)Lgg/essential/elementa/state/MappedState; + public static synthetic fun bindParent$default (Lgg/essential/elementa/UIComponent;Lgg/essential/elementa/UIComponent;Lgg/essential/elementa/state/State;ZLjava/lang/Integer;ILjava/lang/Object;)Lgg/essential/elementa/UIComponent; + public static synthetic fun bindParent$default (Lgg/essential/elementa/UIComponent;Lgg/essential/elementa/state/State;ZLjava/lang/Integer;ILjava/lang/Object;)Lgg/essential/elementa/UIComponent; public static final fun component1 (Ljava/awt/Color;)I public static final fun component2 (Ljava/awt/Color;)I public static final fun component3 (Ljava/awt/Color;)I public static final fun component4 (Ljava/awt/Color;)I + public static final fun getValue (Lgg/essential/elementa/state/State;Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; + public static final fun getWindow (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/components/Window; public static final fun guiHint (DZ)D + public static final fun guiHint (DZLgg/essential/elementa/UIComponent;)D public static final fun guiHint (FZ)F + public static final fun guiHint (FZLgg/essential/elementa/UIComponent;)F + public static final fun hoveredState (Lgg/essential/elementa/UIComponent;ZZ)Lgg/essential/elementa/state/State; + public static synthetic fun hoveredState$default (Lgg/essential/elementa/UIComponent;ZZILjava/lang/Object;)Lgg/essential/elementa/state/State; public static final fun invisible (Ljava/awt/Color;)Ljava/awt/Color; + public static final fun isInComponentTree (Lgg/essential/elementa/UIComponent;)Z + public static final fun not (Lgg/essential/elementa/state/State;)Lgg/essential/elementa/state/MappedState; + public static final fun onLeftClick (Lgg/essential/elementa/UIComponent;Lkotlin/jvm/functions/Function2;)Lgg/essential/elementa/UIComponent; + public static final fun onSetValueAndNow (Lgg/essential/elementa/state/State;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function0; + public static final fun or (Lgg/essential/elementa/state/State;Lgg/essential/elementa/state/State;)Lgg/essential/elementa/state/MappedState; public static final fun roundToRealPixels (D)D + public static final fun roundToRealPixels (DLgg/essential/elementa/UIComponent;)D public static final fun roundToRealPixels (F)F + public static final fun roundToRealPixels (FLgg/essential/elementa/UIComponent;)F + public static final fun setValue (Lgg/essential/elementa/state/State;Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V public static final fun withAlpha (Ljava/awt/Color;F)Ljava/awt/Color; public static final fun withAlpha (Ljava/awt/Color;I)Ljava/awt/Color; } diff --git a/build.gradle.kts b/build.gradle.kts index 0ee9179a..754418c9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,12 +40,20 @@ dependencies { implementation(prebundle(internal)) // Depending on LWJGL3 instead of 2 so we can choose opengl bindings only - compileOnly("org.lwjgl:lwjgl-opengl:3.3.1") + compileOnly("org.lwjgl:lwjgl-opengl:3.2.2") // Depending on 1.8.9 for all of these because that's the oldest version we support compileOnly(libs.versions.universalcraft.map { "gg.essential:universalcraft-1.8.9-forge:$it" }) { attributes { attribute(common, true) } } compileOnly("com.google.code.gson:gson:2.2.4") + + // For external inspector display on LWJGL2 + compileOnly("org.lwjgl.lwjgl:lwjgl:2.9.3") + compileOnly("org.lwjgl.lwjgl:lwjgl_util:2.9.3") + + // For external inspector display on LWJGL3 + compileOnly("org.lwjgl:lwjgl:3.2.2") + compileOnly("org.lwjgl:lwjgl-glfw:3.2.2") } apiValidation { diff --git a/src/main/java/com/example/examplemod/ComponentsGui.kt b/src/main/java/com/example/examplemod/ComponentsGui.kt index f6731ede..126a5183 100644 --- a/src/main/java/com/example/examplemod/ComponentsGui.kt +++ b/src/main/java/com/example/examplemod/ComponentsGui.kt @@ -6,25 +6,30 @@ import gg.essential.elementa.components.* import gg.essential.elementa.components.image.BlurHashImage import gg.essential.elementa.components.input.UIMultilineTextInput import gg.essential.elementa.components.input.UITextInput +import gg.essential.elementa.components.inspector.CompactToggle import gg.essential.elementa.components.inspector.Inspector import gg.essential.elementa.constraints.* import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.OutlineEffect -import gg.essential.elementa.markdown.MarkdownComponent +import gg.essential.elementa.markdown.* +import gg.essential.elementa.state.BasicState import java.awt.Color import java.net.URL class ComponentsGui : WindowScreen(ElementaVersion.V2) { init { - ComponentType("UIContainer") { - val bar = UIBlock().constrain { + // Components declared through a delegated properly will have their component + // name set to the property name in the inspector. In this case, the component + // will be called "bar + val containerExample by ComponentType("UIContainer") { + val bar by UIBlock().constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() width = 150.pixels() height = 50.pixels() } childOf this - val container = UIContainer().constrain { + val container by UIContainer().constrain { x = 0.pixels(true) width = ChildBasedSizeConstraint(padding = 2f) height = ChildBasedMaxSizeConstraint() @@ -39,7 +44,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } } childOf window - ComponentType("UIBlock") { + val blockExample by ComponentType("UIBlock") { UIBlock().constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -48,7 +53,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("UIText") { + val textExample by ComponentType("UIText") { UIText("This is my non-wrapping text").constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -70,7 +75,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("UIWrappedText") { + val wrappedTextExample by ComponentType("UIWrappedText") { UIWrappedText("This is my text that is wrapping at 100 pixels!").constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -93,7 +98,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("UIRoundedRectangle") { + val rectangleExample by ComponentType("UIRoundedRectangle") { UIRoundedRectangle(2f).constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -111,7 +116,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("UICircle") { + val circleExample by ComponentType("UICircle") { UICircle().constrain { // These x & y positions describe the CENTER of the circle x = 30.pixels() @@ -128,7 +133,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("UIShape") { + val shapeExample by ComponentType("UIShape") { val shapeHolder = UIContainer().constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -166,7 +171,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } } childOf window - ComponentType("UIImage") { + val imageExample by ComponentType("UIImage") { UIImage.ofURL(URL("https://i.imgur.com/Pc6iMw3.png")).constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -184,7 +189,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("BlurHashImage") { + val blurHashImageExample by ComponentType("BlurHashImage") { BlurHashImage("L4ESU,OD1e#:=GwwJSAr1M,r|]Ar").constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -202,8 +207,8 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("Text Input") { - val box1 = UIBlock(Color(50, 50, 50)).constrain { + val textInputExample by ComponentType("Text Input") { + val box1 by UIBlock(Color(50, 50, 50)).constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -211,7 +216,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { height = 12.pixels() } childOf this - val textInput1 = UITextInput("My single line text input!").constrain { + val textInput1 by UITextInput("My single line text input!").constrain { x = 2.pixels() y = 2.pixels() @@ -220,7 +225,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { box1.onMouseClick { textInput1.grabWindowFocus() } - val box2 = UIBlock(Color(50, 50, 50)).constrain { + val box2 by UIBlock(Color(50, 50, 50)).constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -228,7 +233,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { height = ChildBasedSizeConstraint() + 4.pixels() } childOf this - val textInput2 = UIMultilineTextInput("My multiline text input!").constrain { + val textInput2 by UIMultilineTextInput("My multiline text input!").constrain { x = 2.pixels() y = 2.pixels() @@ -238,8 +243,8 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { box2.onMouseClick { textInput2.grabWindowFocus() } } childOf window - ComponentType("ScrollComponent") { - val scroll1 = ScrollComponent().constrain { + val scrollComponentExample by ComponentType("ScrollComponent") { + val scroll1 by ScrollComponent().constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -266,7 +271,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("Markdown") { + val markdownExample by ComponentType("Markdown") { MarkdownComponent( """ # Markdown! @@ -285,7 +290,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("SVG") { + val svgExample by ComponentType("SVG") { SVGComponent.ofResource("/svg/test.svg").constrain { x = 2.pixels() y = SiblingConstraint(padding = 2f) @@ -294,7 +299,7 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - ComponentType("Gradient") { + val gradientExample by ComponentType("Gradient") { GradientComponent(Color.BLACK, Color.PINK).constrain { x = 2.pixels() y = SiblingConstraint() + 5.pixels() @@ -303,10 +308,81 @@ class ComponentsGui : WindowScreen(ElementaVersion.V2) { } childOf this } childOf window - Inspector(window).constrain { - x = 10.pixels(true) - y = 10.pixels(true) + val inspectorExample by ComponentType("Inspector") { + constrain { + width = 250.pixels + } + val startDetached = Inspector.startDetached + Inspector.startDetached = false + val inspector = Inspector(window) + Inspector.startDetached = startDetached + + val inspectorTextDescription by MarkdownComponent( + """The Elementa inspector provides useful debug features for creating UIs with Elementa. + It can operate within a Window as an overlay or in an external window. + It's initial position can be configured by adding + `-Delementa.inspector.detached=` to the JVM arguments. + """, + config = MarkdownConfig( + paragraphConfig = ParagraphConfig( + spaceBetweenLines = 0f + ), + inlineCodeConfig = InlineCodeConfig( + backgroundColor = Color.GRAY, + outlineWidth = 0f, + verticalPadding = 1f, + ) + ) + ).constrain { + width = 100.percent + y = SiblingConstraint(2f) + } childOf this + + val inspectorExternal = BasicState(false) + + val toggleRow by UIContainer().constrain { + y = SiblingConstraint(10f) + width = 100.percent + height = ChildBasedMaxSizeConstraint() + }.addChildren( + UIText("Detached Inspector: "), + CompactToggle(inspectorExternal).constrain { + x = SiblingConstraint(5f) + y = CenterConstraint() + } + ) childOf this + + + inspectorExternal.onSetValue { + inspector.setDetached(it) + } + + val inspectorFeaturesDescription by UIWrappedText( + "The inspector allows you to configure the underlying states in components and constraints that" + + " implement the StateRegistry. " + ).constrain { + width = 100.percent + y = SiblingConstraint(10f) + } childOf this + + val inspectorHotkeyDescription by UIWrappedText( + "You can also use hotkeys to interact with the inspector while using your UI.\n" + + "'C' activtes the constraints tab\n" + + "'V' activates the values tab\n" + + "'B' activates the states tab\n" + + "'S' activates the selection tool\n" + + "'M' activates the measure tool\n" + + "'N' disables the measure tool\n" + + "'D' toggles Elementa debug mode\n" + ).constrain { + width = 100.percent + y = SiblingConstraint(10f) + } childOf this + + inspector childOf window + } childOf window + } class ComponentType(componentName: String, initBlock: ComponentType.() -> Unit) : UIContainer() { diff --git a/src/main/kotlin/gg/essential/elementa/UIComponent.kt b/src/main/kotlin/gg/essential/elementa/UIComponent.kt index bd941cce..eb1343fc 100644 --- a/src/main/kotlin/gg/essential/elementa/UIComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/UIComponent.kt @@ -16,8 +16,6 @@ import gg.essential.elementa.utils.* import gg.essential.elementa.utils.requireMainThread import gg.essential.elementa.utils.requireState import gg.essential.universal.UMatrixStack -import gg.essential.universal.UMouse -import gg.essential.universal.UResolution import org.lwjgl.opengl.GL11 import java.awt.Color import java.util.* @@ -383,13 +381,15 @@ abstract class UIComponent : Observable() { } protected fun getMousePosition(): Pair { - return pixelCoordinatesToPixelCenter(UMouse.Scaled.x, UMouse.Scaled.y).let { (x, y) -> x.toFloat() to y.toFloat() } + return pixelCoordinatesToPixelCenter(mousePositionManager.scaledX, mousePositionManager.scaledY) + .let { (x, y) -> x.toFloat() to y.toFloat() } } internal fun pixelCoordinatesToPixelCenter(mouseX: Double, mouseY: Double): Pair { // Move the position of a click to the center of a pixel. See [ElementaVersion.v2] for more info - return if ((Window.ofOrNull(this)?.version ?: ElementaVersion.v0) >= ElementaVersion.v2) { - val halfPixel = 0.5 / UResolution.scaleFactor + val window = Window.ofOrNull(this) + return if (window !=null && window.version >= ElementaVersion.v2) { + val halfPixel = 0.5 / window.resolutionManager.scaleFactor mouseX + halfPixel to mouseY + halfPixel } else { mouseX to mouseY @@ -1237,8 +1237,9 @@ abstract class UIComponent : Observable() { /** * Hints a number with respect to the current GUI scale. */ +// @Deprecated("This relies on global states", replaceWith = ReplaceWith("guiHint(number, roundDown, component)")) fun guiHint(number: Float, roundDown: Boolean): Float { - val factor = UResolution.scaleFactor.toFloat() + val factor = Window.resolutionManager.scaleFactor.toFloat() return (number * factor).let { if (roundDown) floor(it) else ceil(it) } / factor @@ -1247,19 +1248,33 @@ abstract class UIComponent : Observable() { /** * Hints a number with respect to the current GUI scale. */ - fun guiHint(number: Double, roundDown: Boolean): Double { - val factor = UResolution.scaleFactor + fun guiHint(number: Float, roundDown: Boolean, component: UIComponent): Float { + val factor = component.resolutionManager.scaleFactor.toFloat() return (number * factor).let { if (roundDown) floor(it) else ceil(it) } / factor } - internal fun getMouseX(): Float { - return UMouse.Scaled.x.toFloat() + /** + * Hints a number with respect to the current GUI scale. + */ +// @Deprecated("This relies on global states", replaceWith = ReplaceWith("guiHint(number, roundDown, component)")) + fun guiHint(number: Double, roundDown: Boolean): Double { + val factor = Window.resolutionManager.scaleFactor + return (number * factor).let { + if (roundDown) floor(it) else ceil(it) + } / factor } - internal fun getMouseY(): Float { - return UMouse.Scaled.y.toFloat() + /** + * Hints a number with respect to the current GUI scale. + */ + fun guiHint(number: Double, roundDown: Boolean, component: UIComponent): Double { + val factor = component.resolutionManager.scaleFactor + return (number * factor).let { + if (roundDown) floor(it) else ceil(it) + } / factor } + } } diff --git a/src/main/kotlin/gg/essential/elementa/VanillaFontRenderer.kt b/src/main/kotlin/gg/essential/elementa/VanillaFontRenderer.kt index c693a82f..20fe6820 100644 --- a/src/main/kotlin/gg/essential/elementa/VanillaFontRenderer.kt +++ b/src/main/kotlin/gg/essential/elementa/VanillaFontRenderer.kt @@ -22,6 +22,7 @@ class VanillaFontRenderer : FontProvider { override fun getStringHeight(string: String, pointSize: Float): Float = UGraphics.getFontHeight().toFloat() + @Suppress("DEPRECATION") override fun drawString( matrixStack: UMatrixStack, string: String, diff --git a/src/main/kotlin/gg/essential/elementa/WindowScreen.kt b/src/main/kotlin/gg/essential/elementa/WindowScreen.kt index bfacc28e..78a8ee32 100644 --- a/src/main/kotlin/gg/essential/elementa/WindowScreen.kt +++ b/src/main/kotlin/gg/essential/elementa/WindowScreen.kt @@ -4,7 +4,6 @@ import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.animation.* import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack -import gg.essential.universal.UMouse import gg.essential.universal.UScreen import java.awt.Color @@ -67,8 +66,8 @@ abstract class WindowScreen @JvmOverloads constructor( // See [ElementaVersion.V2] for more info val (adjustedMouseX, adjustedMouseY) = if (version >= ElementaVersion.v2 && (mouseX == floor(mouseX) && mouseY == floor(mouseY))) { - val x = UMouse.Scaled.x - val y = UMouse.Scaled.y + val x = window.mousePositionManager.scaledX + val y = window.mousePositionManager.scaledY mouseX + (x - floor(x)) to mouseY + (y - floor(y)) } else { @@ -107,7 +106,7 @@ abstract class WindowScreen @JvmOverloads constructor( // to type. This is a wrapper around a base LWJGL function. // - Keyboard.enableRepeatEvents in <= 1.12.2 if (enableRepeatKeys) - UKeyboard.allowRepeatEvents(true) + window.keyboardManager.allowRepeatEvents(true) } override fun onScreenClose() { @@ -115,11 +114,11 @@ abstract class WindowScreen @JvmOverloads constructor( // We need to disable repeat events when leaving the gui. if (enableRepeatKeys) - UKeyboard.allowRepeatEvents(false) + window.keyboardManager.allowRepeatEvents(false) } fun defaultKeyBehavior(typedChar: Char, keyCode: Int) { - super.onKeyPressed(keyCode, typedChar, UKeyboard.getModifiers()) + super.onKeyPressed(keyCode, typedChar, window.keyboardManager.getModifiers()) } /** diff --git a/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt b/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt index 2221c180..2b3c141a 100644 --- a/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt @@ -7,9 +7,9 @@ import gg.essential.elementa.constraints.resolution.ConstraintVisitor import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.ScissorEffect import gg.essential.elementa.utils.bindLast -import gg.essential.universal.UKeyboard +import gg.essential.elementa.utils.keyboardManager +import gg.essential.elementa.utils.mousePositionManager import gg.essential.universal.UMatrixStack -import gg.essential.universal.UMouse import java.awt.Color import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs @@ -112,9 +112,9 @@ class ScrollComponent @JvmOverloads constructor( scrollIconComponent.hide(instantly = true) onMouseScroll { - if (UKeyboard.isShiftKeyDown() && horizontalScrollEnabled) { + if (keyboardManager.isShiftKeyDown() && horizontalScrollEnabled) { onScroll(it.delta.toFloat(), isHorizontal = true) - } else if (!UKeyboard.isShiftKeyDown() && verticalScrollEnabled) { + } else if (!keyboardManager.isShiftKeyDown() && verticalScrollEnabled) { onScroll(it.delta.toFloat(), isHorizontal = false) } @@ -229,7 +229,7 @@ class ScrollComponent @JvmOverloads constructor( } component.onMouseScroll { - if (isHorizontal && horizontalScrollEnabled && UKeyboard.isShiftKeyDown()) { + if (isHorizontal && horizontalScrollEnabled && keyboardManager.isShiftKeyDown()) { onScroll(it.delta.toFloat(), isHorizontal = true) } else if (!isHorizontal && verticalScrollEnabled) { onScroll(it.delta.toFloat(), isHorizontal = false) @@ -498,7 +498,7 @@ class ScrollComponent @JvmOverloads constructor( if (horizontalScrollEnabled) { val xBegin = autoScrollBegin.first + getLeft() - val currentX = UMouse.Scaled.x + val currentX = mousePositionManager.scaledX if (currentX in getLeft()..getRight()) { val deltaX = currentX - xBegin @@ -510,7 +510,7 @@ class ScrollComponent @JvmOverloads constructor( if (verticalScrollEnabled) { val yBegin = autoScrollBegin.second + getTop() - val currentY = UMouse.Scaled.y + val currentY = mousePositionManager.scaledY if (currentY in getTop()..getBottom()) { val deltaY = currentY - yBegin diff --git a/src/main/kotlin/gg/essential/elementa/components/TreeListComponent.kt b/src/main/kotlin/gg/essential/elementa/components/TreeListComponent.kt index d811e9a2..a37fff93 100644 --- a/src/main/kotlin/gg/essential/elementa/components/TreeListComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/components/TreeListComponent.kt @@ -6,7 +6,7 @@ import gg.essential.elementa.dsl.basicWidthConstraint import gg.essential.elementa.dsl.childOf import gg.essential.elementa.dsl.constrain import gg.essential.elementa.dsl.pixels -import gg.essential.universal.UKeyboard +import gg.essential.elementa.utils.keyboardManager import kotlin.math.max abstract class TreeArrowComponent : UIComponent() { @@ -124,7 +124,7 @@ abstract class TreeNode { arrowComponent.onMouseClick { event -> event.stopImmediatePropagation() - val isRecursive = UKeyboard.isShiftKeyDown() + val isRecursive = keyboardManager.isShiftKeyDown() if (opened) { close(isRecursive) diff --git a/src/main/kotlin/gg/essential/elementa/components/UIText.kt b/src/main/kotlin/gg/essential/elementa/components/UIText.kt index 9a3a43fd..b148a741 100644 --- a/src/main/kotlin/gg/essential/elementa/components/UIText.kt +++ b/src/main/kotlin/gg/essential/elementa/components/UIText.kt @@ -3,6 +3,8 @@ package gg.essential.elementa.components import gg.essential.elementa.UIComponent import gg.essential.elementa.UIConstraints import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.dsl.width import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.MappedState @@ -10,6 +12,7 @@ import gg.essential.elementa.state.State import gg.essential.elementa.state.pixels import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus import java.awt.Color /** @@ -20,12 +23,12 @@ open class UIText constructor( text: State, shadow: State = BasicState(true), - shadowColor: State = BasicState(null) -) : UIComponent() { + shadowColor: State = BasicState(null), +) : UIComponent(), StateRegistry { @JvmOverloads constructor( text: String = "", shadow: Boolean = true, - shadowColor: Color? = null + shadowColor: Color? = null, ) : this(BasicState(text), BasicState(shadow), BasicState(shadowColor)) private val textState: MappedState = text.map { it } // extra map so we can easily rebind it @@ -57,6 +60,15 @@ constructor( }.pixels()) } + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfString(textState, "text", true), + ManagedState.OfBoolean(shadowState, "shadow", true), + ManagedState.OfColorOrNull(shadowColorState, "shadowColor", true), + ) + + fun bindText(newTextState: State) = apply { textState.rebind(newTextState) } diff --git a/src/main/kotlin/gg/essential/elementa/components/UIWrappedText.kt b/src/main/kotlin/gg/essential/elementa/components/UIWrappedText.kt index 53be4cf1..8b4b720e 100644 --- a/src/main/kotlin/gg/essential/elementa/components/UIWrappedText.kt +++ b/src/main/kotlin/gg/essential/elementa/components/UIWrappedText.kt @@ -3,6 +3,8 @@ package gg.essential.elementa.components import gg.essential.elementa.UIComponent import gg.essential.elementa.UIConstraints import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.dsl.basicHeightConstraint import gg.essential.elementa.dsl.width import gg.essential.elementa.state.BasicState @@ -13,6 +15,7 @@ import gg.essential.elementa.utils.getStringSplitToWidth import gg.essential.elementa.utils.getStringSplitToWidthTruncated import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus import java.awt.Color /** @@ -31,7 +34,7 @@ open class UIWrappedText @JvmOverloads constructor( private val trimText: Boolean = false, private val lineSpacing: Float = 9f, private val trimmedTextSuffix: String = "..." -) : UIComponent() { +) : UIComponent(), StateRegistry { @JvmOverloads constructor( text: String = "", shadow: Boolean = true, @@ -101,6 +104,14 @@ open class UIWrappedText @JvmOverloads constructor( }) } + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfString(textState, "text", true), + ManagedState.OfBoolean(shadowState, "shadow", true), + ManagedState.OfColorOrNull(shadowColorState, "shadowColor", true), + ) + fun bindText(newTextState: State) = apply { textState.rebind(newTextState) } diff --git a/src/main/kotlin/gg/essential/elementa/components/Window.kt b/src/main/kotlin/gg/essential/elementa/components/Window.kt index c2d86746..35b213a6 100644 --- a/src/main/kotlin/gg/essential/elementa/components/Window.kt +++ b/src/main/kotlin/gg/essential/elementa/components/Window.kt @@ -8,9 +8,16 @@ import gg.essential.elementa.constraints.resolution.ConstraintResolverV2 import gg.essential.elementa.effects.ScissorEffect import gg.essential.elementa.font.FontRenderer import gg.essential.elementa.impl.Platform.Companion.platform +import gg.essential.elementa.manager.* +import gg.essential.elementa.manager.DefaultMousePositionManager +import gg.essential.elementa.manager.DefaultResolutionManager +import gg.essential.elementa.manager.KeyboardManager +import gg.essential.elementa.manager.MousePositionManager +import gg.essential.elementa.manager.ResolutionManager import gg.essential.elementa.utils.elementaDev import gg.essential.elementa.utils.requireMainThread import gg.essential.universal.* +import org.jetbrains.annotations.ApiStatus import org.lwjgl.opengl.GL11 import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.TimeUnit @@ -37,6 +44,18 @@ class Window @JvmOverloads constructor( internal var clickInterceptor: ((mouseX: Double, mouseY: Double, button: Int) -> Boolean)? = null + internal val drawCallbacks = mutableListOf Unit>() + + /** + * State managers to avoid global states + */ + @ApiStatus.Internal + var resolutionManager: ResolutionManager = DefaultResolutionManager + @ApiStatus.Internal + var mousePositionManager: MousePositionManager = DefaultMousePositionManager + @ApiStatus.Internal + var keyboardManager: KeyboardManager = DefaultKeyboardManager + @Deprecated("Add ElementaVersion as the first argument to opt-in to improved behavior.") @JvmOverloads constructor(animationFPS: Int = 244) : this(ElementaVersion.v0, animationFPS) @@ -75,6 +94,8 @@ class Window @JvmOverloads constructor( try { + currentWindow.set(this) + //If this Window is more than 5 seconds behind, reset it be only 5 seconds. //This will drop missed frames but avoid the game freezing as the Window tries //to catch after a period of inactivity @@ -136,7 +157,10 @@ class Window @JvmOverloads constructor( null } } + } finally { + currentWindow.set(null) } + drawCallbacks.forEach{ it() } } internal fun drawEmbedded(matrixStack: UMatrixStack) { @@ -269,11 +293,11 @@ class Window @JvmOverloads constructor( } override fun getWidth(): Float { - return UResolution.scaledWidth.toFloat() + return resolutionManager.scaledWidth.toFloat() } override fun getHeight(): Float { - return UResolution.scaledHeight.toFloat() + return resolutionManager.scaledHeight.toFloat() } override fun getRight() = getWidth() @@ -287,12 +311,12 @@ class Window @JvmOverloads constructor( ) return false val currentScissor = ScissorEffect.currentScissorState ?: return true - val sf = UResolution.scaleFactor + val sf = resolutionManager.scaleFactor val realX = currentScissor.x / sf val realWidth = currentScissor.width / sf - val bottomY = ((UResolution.scaledHeight * sf) - currentScissor.y) / sf + val bottomY = ((resolutionManager.scaledHeight * sf) - currentScissor.y) / sf val realHeight = currentScissor.height / sf return right > realX && @@ -368,6 +392,32 @@ class Window @JvmOverloads constructor( companion object { private val renderOperations = ConcurrentLinkedQueue<() -> Unit>() + /** + * Instance of the Window currently being rendered + */ + internal val currentWindow: ThreadLocal = ThreadLocal.withInitial { null } + + /** + * Resolution manager of the window currently being rendered or [DefaultResolutionManager] + * if one cannot be resolved. + */ + internal val resolutionManager: ResolutionManager + get() = currentWindow.get()?.resolutionManager ?: DefaultResolutionManager + + /** + * Mouse position manger of the window currently being rendered or [DefaultMousePositionManager] + * if one cannot be resolved. + */ + internal val mousePositionManager: MousePositionManager + get() = currentWindow.get()?.mousePositionManager ?: DefaultMousePositionManager + + /** + * Keyboard manager of the window currently being rendered or [DefaultKeyboardManager] + * if one cannot be resolved. + */ + internal val keyboardManager: KeyboardManager + get() = currentWindow.get()?.keyboardManager ?: DefaultKeyboardManager + fun enqueueRenderOperation(operation: Runnable) { renderOperations.add { operation.run() diff --git a/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt b/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt index 4ae4ddc0..4a900d49 100644 --- a/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt +++ b/src/main/kotlin/gg/essential/elementa/components/input/AbstractTextInput.kt @@ -8,6 +8,7 @@ import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.ScissorEffect import gg.essential.elementa.impl.Platform.Companion.platform import gg.essential.elementa.utils.getStringSplitToWidth +import gg.essential.elementa.utils.keyboardManager import gg.essential.universal.UDesktop import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack @@ -83,22 +84,22 @@ abstract class AbstractTextInput( if (keyCode == UKeyboard.KEY_ESCAPE) { releaseWindowFocus() - } else if (UKeyboard.isKeyComboCtrlA(keyCode)) { + } else if (keyboardManager.isKeyComboCtrlA(keyCode)) { selectAll() - } else if (UKeyboard.isKeyComboCtrlC(keyCode) && hasSelection()) { + } else if (keyboardManager.isKeyComboCtrlC(keyCode) && hasSelection()) { copySelection() - } else if (UKeyboard.isKeyComboCtrlX(keyCode) && hasSelection()) { + } else if (keyboardManager.isKeyComboCtrlX(keyCode) && hasSelection()) { copySelection() deleteSelection() - } else if (UKeyboard.isKeyComboCtrlV(keyCode)) { + } else if (keyboardManager.isKeyComboCtrlV(keyCode)) { commitTextAddition(UDesktop.getClipboardString()) - } else if (UKeyboard.isKeyComboCtrlZ(keyCode)) { + } else if (keyboardManager.isKeyComboCtrlZ(keyCode)) { if (undoStack.isEmpty()) return@onKeyType val operationToUndo = undoStack.pop() operationToUndo.undo() redoStack.push(operationToUndo) - } else if (UKeyboard.isKeyComboCtrlShiftZ(keyCode) || UKeyboard.isKeyComboCtrlY(keyCode)) { + } else if (keyboardManager.isKeyComboCtrlShiftZ(keyCode) || keyboardManager.isKeyComboCtrlY(keyCode)) { if (redoStack.isEmpty()) return@onKeyType val operationToRedo = redoStack.pop() @@ -107,8 +108,8 @@ abstract class AbstractTextInput( } else if (platform.isAllowedInChat(typedChar)) { // Most of the ASCII characters commitTextAddition(typedChar.toString()) } else if (keyCode == UKeyboard.KEY_LEFT) { - val holdingShift = UKeyboard.isShiftKeyDown() - val holdingCtrl = UKeyboard.isCtrlKeyDown() + val holdingShift = keyboardManager.isShiftKeyDown() + val holdingCtrl = keyboardManager.isCtrlKeyDown() val newCursorPosition = when { holdingCtrl -> getNearestWordBoundary(cursor, Direction.Left) @@ -124,8 +125,8 @@ abstract class AbstractTextInput( cursor = newCursorPosition cursorNeedsRefocus = true } else if (keyCode == UKeyboard.KEY_RIGHT) { - val holdingShift = UKeyboard.isShiftKeyDown() - val holdingCtrl = UKeyboard.isCtrlKeyDown() + val holdingShift = keyboardManager.isShiftKeyDown() + val holdingCtrl = keyboardManager.isCtrlKeyDown() val newCursorPosition = when { holdingCtrl -> getNearestWordBoundary(cursor, Direction.Right) @@ -148,7 +149,7 @@ abstract class AbstractTextInput( screenPosToVisualPos(currX, currY - lineHeight) } - if (UKeyboard.isShiftKeyDown()) { + if (keyboardManager.isShiftKeyDown()) { cursor = newVisualPos cursorNeedsRefocus = true } else { @@ -162,7 +163,7 @@ abstract class AbstractTextInput( screenPosToVisualPos(currX, currY + lineHeight) } - if (UKeyboard.isShiftKeyDown()) { + if (keyboardManager.isShiftKeyDown()) { cursor = newVisualPos cursorNeedsRefocus = true } else { @@ -172,7 +173,7 @@ abstract class AbstractTextInput( if (hasSelection()) { deleteSelection() } else if (!cursor.isAtAbsoluteStart) { - val startPos = if (UKeyboard.isCtrlKeyDown()) { + val startPos = if (keyboardManager.isCtrlKeyDown()) { getNearestWordBoundary(cursor, Direction.Left) } else cursor.offsetColumn(-1).toTextualPos() val endPos = cursor.toTextualPos() @@ -184,14 +185,14 @@ abstract class AbstractTextInput( deleteSelection() } else if (!cursor.isAtAbsoluteEnd) { val startPos = cursor.toTextualPos() - val endPos = if (UKeyboard.isCtrlKeyDown()) { + val endPos = if (keyboardManager.isCtrlKeyDown()) { getNearestWordBoundary(cursor, Direction.Right) } else cursor.offsetColumn(1).toTextualPos() commitTextRemoval(startPos, endPos, selectAfterUndo = false) } } else if (keyCode == UKeyboard.KEY_HOME) { - if (UKeyboard.isShiftKeyDown()) { + if (keyboardManager.isShiftKeyDown()) { cursor = cursor.withColumn(0) cursorNeedsRefocus = true } else { @@ -199,7 +200,7 @@ abstract class AbstractTextInput( } } else if (keyCode == UKeyboard.KEY_END) { cursor.withColumn(visualLines[cursor.line].length).also { - if (UKeyboard.isShiftKeyDown()) { + if (keyboardManager.isShiftKeyDown()) { cursor = it cursorNeedsRefocus = true } else { diff --git a/src/main/kotlin/gg/essential/elementa/components/input/UIMultilineTextInput.kt b/src/main/kotlin/gg/essential/elementa/components/input/UIMultilineTextInput.kt index e76b88a1..51702152 100644 --- a/src/main/kotlin/gg/essential/elementa/components/input/UIMultilineTextInput.kt +++ b/src/main/kotlin/gg/essential/elementa/components/input/UIMultilineTextInput.kt @@ -5,7 +5,7 @@ import gg.essential.elementa.dsl.coerceAtMost import gg.essential.elementa.dsl.pixels import gg.essential.elementa.dsl.width import gg.essential.elementa.utils.getStringSplitToWidthTruncated -import gg.essential.universal.UKeyboard +import gg.essential.elementa.utils.keyboardManager import gg.essential.universal.UMatrixStack import java.awt.Color @@ -64,7 +64,7 @@ class UIMultilineTextInput @JvmOverloads constructor( } override fun onEnterPressed() { - if (UKeyboard.isShiftKeyDown()) { + if (keyboardManager.isShiftKeyDown()) { commitTextAddition("\n") updateAction(getText()) } else { diff --git a/src/main/kotlin/gg/essential/elementa/components/input/v2/AbstractTextInput.kt b/src/main/kotlin/gg/essential/elementa/components/input/v2/AbstractTextInput.kt new file mode 100644 index 00000000..f54e5788 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/input/v2/AbstractTextInput.kt @@ -0,0 +1,1021 @@ +package gg.essential.elementa.components.input.v2 + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.* +import gg.essential.elementa.effects.ScissorEffect +import gg.essential.elementa.impl.Platform.Companion.platform +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.utils.getStringSplitToWidth +import gg.essential.elementa.utils.keyboardManager +import gg.essential.universal.UDesktop +import gg.essential.universal.UKeyboard +import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus +import java.awt.Color +import java.util.* +import kotlin.math.abs + +@ApiStatus.Internal +abstract class AbstractTextInput( + var placeholder: String, + var contentShadow: Boolean, + var contentShadowColor: Color?, + protected val selectionBackgroundColor: Color, + protected var selectionForegroundColor: Color, + protected val allowInactiveSelection: Boolean, + protected val inactiveSelectionBackgroundColor: Color, + protected var inactiveSelectionForegroundColor: Color, + protected val cursorColor: Color +) : UIComponent() { + + val placeholderColor = BasicState(Color(0xBBBBBB)) + val placeholderShadow = BasicState(true) + val textState = BasicState("") + + protected var active = false + var lineHeight = 9f + set(value) { + cursorComponent.setHeight(value.pixels()) + this.setHeight(value.pixels()) + field = value + } + protected var updateAction: (text: String) -> Unit = {} + protected var activateAction: (text: String) -> Unit = {} + + protected val textualLines = mutableListOf(TextualLine("", 0..0)) + protected val visualLines = mutableListOf(VisualLine("", 0)) + + protected var verticalScrollingOffset = 0f + protected var targetVerticalScrollingOffset = 0f + protected var horizontalScrollingOffset = 0f + protected var cursorNeedsRefocus = false + + protected var lastSelectionMoveTimestamp = System.currentTimeMillis() + protected var selectionMode = SelectionMode.None + protected var initiallySelectedLine = -1 + protected var initiallySelectedWord = LinePosition(0, 0, true) to LinePosition(0, 0, true) + + protected val undoStack = ArrayDeque() + protected val redoStack = ArrayDeque() + + protected var cursorComponent: UIComponent = UIBlock(Color(255, 255, 255, 0)).constrain { + y = CenterConstraint() - 0.5f.pixels() + width = 1.pixel() + height = lineHeight.pixels() + } childOf this + + protected var cursor = LinePosition(0, 0, isVisual = true) + set(value) { + field = value.toVisualPos() + } + + protected var otherSelectionEnd = LinePosition(0, 0, isVisual = true) + set(value) { + field = value.toVisualPos() + } + + @ApiStatus.Internal + enum class SelectionMode { + None, + Character, + Word, + Line, + } + + init { + + setHeight(lineHeight.pixels()) + + onKeyType { typedChar, keyCode -> + if (!active) return@onKeyType + + if (keyCode == UKeyboard.KEY_ESCAPE) { + releaseWindowFocus() + } else if (keyboardManager.isKeyComboCtrlA(keyCode)) { + selectAll() + } else if (keyboardManager.isKeyComboCtrlC(keyCode) && hasSelection()) { + copySelection() + } else if (keyboardManager.isKeyComboCtrlX(keyCode) && hasSelection()) { + copySelection() + deleteSelection() + } else if (keyboardManager.isKeyComboCtrlV(keyCode)) { + commitTextAddition(UDesktop.getClipboardString()) + } else if (keyboardManager.isKeyComboCtrlZ(keyCode)) { + if (undoStack.isEmpty()) + return@onKeyType + val operationToUndo = undoStack.pop() + operationToUndo.undo() + redoStack.push(operationToUndo) + } else if (keyboardManager.isKeyComboCtrlShiftZ(keyCode) || keyboardManager.isKeyComboCtrlY(keyCode)) { + if (redoStack.isEmpty()) + return@onKeyType + val operationToRedo = redoStack.pop() + operationToRedo.redo() + undoStack.push(operationToRedo) + } else if (platform.isAllowedInChat(typedChar)) { // Most of the ASCII characters + commitTextAddition(typedChar.toString()) + } else if (keyCode == UKeyboard.KEY_LEFT) { + val holdingShift = keyboardManager.isShiftKeyDown() + val holdingCtrl = keyboardManager.isCtrlKeyDown() + + val newCursorPosition = when { + holdingCtrl -> getNearestWordBoundary(cursor, Direction.Left) + hasSelection() -> if (holdingShift) cursor.offsetColumn(-1) else selectionStart() + else -> cursor.offsetColumn(-1) + } + + if (!holdingShift) { + setCursorPosition(newCursorPosition) + return@onKeyType + } + + cursor = newCursorPosition + cursorNeedsRefocus = true + } else if (keyCode == UKeyboard.KEY_RIGHT) { + val holdingShift = keyboardManager.isShiftKeyDown() + val holdingCtrl = keyboardManager.isCtrlKeyDown() + + val newCursorPosition = when { + holdingCtrl -> getNearestWordBoundary(cursor, Direction.Right) + hasSelection() -> if (holdingShift) cursor.offsetColumn(1) else selectionEnd() + else -> cursor.offsetColumn(1) + } + + if (!holdingShift) { + setCursorPosition(newCursorPosition) + return@onKeyType + } + + cursor = newCursorPosition + cursorNeedsRefocus = true + } else if (keyCode == UKeyboard.KEY_UP) { + val newVisualPos = if (cursor.line == 0) { + LinePosition(0, 0, isVisual = true) + } else { + val (currX, currY) = cursor.toScreenPos() + screenPosToVisualPos(currX, currY - lineHeight) + } + + if (keyboardManager.isShiftKeyDown()) { + cursor = newVisualPos + cursorNeedsRefocus = true + } else { + setCursorPosition(newVisualPos) + } + } else if (keyCode == UKeyboard.KEY_DOWN) { + val newVisualPos = if (cursor.line == visualLines.lastIndex) { + LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true) + } else { + val (currX, currY) = cursor.toScreenPos() + screenPosToVisualPos(currX, currY + lineHeight) + } + + if (keyboardManager.isShiftKeyDown()) { + cursor = newVisualPos + cursorNeedsRefocus = true + } else { + setCursorPosition(newVisualPos) + } + } else if (keyCode == UKeyboard.KEY_BACKSPACE) { + if (hasSelection()) { + deleteSelection() + } else if (!cursor.isAtAbsoluteStart) { + val startPos = if (keyboardManager.isCtrlKeyDown()) { + getNearestWordBoundary(cursor, Direction.Left) + } else cursor.offsetColumn(-1).toTextualPos() + val endPos = cursor.toTextualPos() + + commitTextRemoval(startPos, endPos, selectAfterUndo = false) + } + } else if (keyCode == UKeyboard.KEY_DELETE) { + if (hasSelection()) { + deleteSelection() + } else if (!cursor.isAtAbsoluteEnd) { + val startPos = cursor.toTextualPos() + val endPos = if (keyboardManager.isCtrlKeyDown()) { + getNearestWordBoundary(cursor, Direction.Right) + } else cursor.offsetColumn(1).toTextualPos() + + commitTextRemoval(startPos, endPos, selectAfterUndo = false) + } + } else if (keyCode == UKeyboard.KEY_HOME) { + if (keyboardManager.isShiftKeyDown()) { + cursor = cursor.withColumn(0) + cursorNeedsRefocus = true + } else { + setCursorPosition(cursor.withColumn(0)) + } + } else if (keyCode == UKeyboard.KEY_END) { + cursor.withColumn(visualLines[cursor.line].length).also { + if (keyboardManager.isShiftKeyDown()) { + cursor = it + cursorNeedsRefocus = true + } else { + setCursorPosition(it) + } + } + } else if (keyCode == UKeyboard.KEY_ENTER) { // Enter + onEnterPressed() + } + } + + onMouseScroll { + val heightDifference = getHeight() - visualLines.size * lineHeight + if (heightDifference > 0) + return@onMouseScroll + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset + it.delta.toFloat() * lineHeight).coerceIn(heightDifference, 0f) + it.stopPropagation() + } + + onMouseClick { event -> + if (!active || event.mouseButton != 0) + return@onMouseClick + + val clickedVisualPos = screenPosToVisualPos(event.relativeX, event.relativeY) + + var clickCount = event.clickCount % 3 + if (clickCount == 0 && clickedVisualPos.line != cursor.line) + clickCount = 1 + else if (clickCount == 2 && cursor != clickedVisualPos) + clickCount = 1 + + when (clickCount) { + 0 -> { + selectionMode = SelectionMode.Line + otherSelectionEnd = clickedVisualPos.withColumn(visualLines[cursor.line].length) + initiallySelectedLine = cursor.line + } + 1 -> { + selectionMode = SelectionMode.Character + setCursorPosition(clickedVisualPos) + } + 2 -> { + selectionMode = SelectionMode.Word + cursor = getNearestWordBoundary( + clickedVisualPos, + Direction.Left + ) + cursorNeedsRefocus = true + otherSelectionEnd = getNearestWordBoundary( + clickedVisualPos, + Direction.Right + ) + initiallySelectedWord = cursor to otherSelectionEnd + } + } + } + + onMouseDrag { mouseX, mouseY, mouseButton -> + if (mouseButton != 0 || selectionMode == SelectionMode.None) + return@onMouseDrag + + val draggedVisualPos = screenPosToVisualPos(mouseX, mouseY) + + when (selectionMode) { + SelectionMode.Character -> otherSelectionEnd = draggedVisualPos + SelectionMode.Line -> if (initiallySelectedLine < draggedVisualPos.line) { + cursor = LinePosition(initiallySelectedLine, 0, isVisual = true) + otherSelectionEnd = draggedVisualPos.withColumn(visualLines[draggedVisualPos.line].length) + } else { + cursor = draggedVisualPos.withColumn(0) + otherSelectionEnd = LinePosition( + initiallySelectedLine, + visualLines[initiallySelectedLine].length, + isVisual = true + ) + } + SelectionMode.Word -> when { + draggedVisualPos < initiallySelectedWord.first -> { + cursor = getNearestWordBoundary( + draggedVisualPos, + Direction.Left + ) + otherSelectionEnd = initiallySelectedWord.second + } + draggedVisualPos > initiallySelectedWord.second -> { + cursor = initiallySelectedWord.first + otherSelectionEnd = getNearestWordBoundary( + draggedVisualPos, + Direction.Right + ) + } + else -> { + cursor = initiallySelectedWord.first + otherSelectionEnd = initiallySelectedWord.second + } + } + SelectionMode.None -> { + } + } + + val currentTime = System.currentTimeMillis() + if (currentTime - lastSelectionMoveTimestamp > 50) { + if (mouseY <= 0) { + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset + lineHeight * getTextScale()).coerceAtMost(0f) + lastSelectionMoveTimestamp = currentTime + } else if (mouseY >= getHeight()) { + val heightDifference = getHeight() - visualLines.size * lineHeight * getTextScale() + targetVerticalScrollingOffset = + (targetVerticalScrollingOffset - lineHeight * getTextScale()).coerceIn(0f, heightDifference) + lastSelectionMoveTimestamp = currentTime + } else if (mouseX <= 0) { + scrollIntoView(draggedVisualPos.offsetColumn(-1)) + lastSelectionMoveTimestamp = currentTime + } else if (mouseX >= getWidth()) { + scrollIntoView(draggedVisualPos.offsetColumn(1)) + lastSelectionMoveTimestamp = currentTime + } + } + } + + onMouseRelease { + selectionMode = SelectionMode.None + } + + onFocus { + setActive(true) + } + + onFocusLost { + setActive(false) + } + + cursorComponent.animateAfterUnhide { + setColorAnimation(Animations.OUT_CIRCULAR, 0.5f, cursorColor.toConstraint()) + onComplete { + if (!active) return@onComplete + cursorComponent.animate { + setColorAnimation(Animations.IN_CIRCULAR, 0.5f, Color(255, 255, 255, 0).toConstraint()) + onComplete { + if (active) animateCursor() + } + } + } + } + + enableEffect(ScissorEffect()) + + textState.onSetValue { + if (it != getText()) { + setText(it) + } + } + } + + override fun draw(matrixStack: UMatrixStack) { + cursorComponent.setHeight( + (lineHeight * getTextScale()).pixels() + ) + super.draw(matrixStack) + } + + abstract fun getText(): String + fun setText(text: String) { + val absoluteStart = LinePosition(0, 0, isVisual = true) + val replaceTextOperation = ReplaceTextOperation( + AddTextOperation(text, absoluteStart), + RemoveTextOperation( + absoluteStart, + LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true), + selectAfterUndo = true + ) + ) + commitTextOperation(replaceTextOperation) + } + + protected abstract fun scrollIntoView(pos: LinePosition) + protected abstract fun screenPosToVisualPos(x: Float, y: Float): LinePosition + protected abstract fun recalculateDimensions() + protected abstract fun textToLines(text: String): List + protected abstract fun onEnterPressed() + + fun setActive(isActive: Boolean) = apply { + active = isActive + + if (isActive) { + cursorComponent.unhide() + animateCursor() + } else { + cursorComponent.setColor(Color(255, 255, 255, 0).toConstraint()) + if (hasText() && (!allowInactiveSelection || !hasSelection())) { + setCursorPosition(LinePosition(visualLines.lastIndex, visualLines.last().length, isVisual = true)) + } + } + } + + fun isActive() = active + + fun onUpdate(listener: (text: String) -> Unit) = apply { + updateAction = listener + } + + fun onActivate(listener: (text: String) -> Unit) = apply { + activateAction = listener + } + + protected open fun commitTextOperation(operation: TextOperation) { + operation.redo() + undoStack.push(operation) + redoStack.clear() + } + + protected fun commitTextAddition(newText: String) { + val addTextOperation = AddTextOperation(newText, cursor) + + if (hasSelection()) { + val removeTextOperation = RemoveTextOperation(selectionStart(), selectionEnd(), selectAfterUndo = true) + val replaceTextOperation = ReplaceTextOperation(addTextOperation, removeTextOperation) + commitTextOperation(replaceTextOperation) + return + } + + commitTextOperation(addTextOperation) + } + + protected fun addText(newText: String, position: LinePosition) { + val textPos = position.toTextualPos() + val textualLine = textualLines[textPos.line] + + val lines = textToLines(newText) + when { + lines.isEmpty() -> { + return + } + lines.size == 1 -> { + textualLine.addTextAt(lines.first(), textPos.column) + } + else -> { + val newTextualLines = lines.drop(1).map { TextualLine(it) } + + if (textPos.column < textualLine.text.length) { + val textAfterInsertion = textualLine.text.substring(textPos.column) + textualLine.text = textualLine.text.substring(0, textPos.column) + lines.first() + newTextualLines.last().text += textAfterInsertion + } else { + textualLine.addTextAt(lines.first(), textPos.column) + } + + textualLines.addAll(textPos.line + 1, newTextualLines) + } + } + + recalculateAllVisualLines() + setCursorPosition(textPos.offsetColumn(newText.length).toVisualPos()) + + updateAction(getText()) + textState.set(getText()) + } + + protected open fun recalculateVisualLinesFor(textualLineIndex: Int) { + val textualLine = textualLines[textualLineIndex] + val firstVisualIndex = textualLine.visualIndices.first + repeat(textualLine.visualIndices.count()) { + if (firstVisualIndex < visualLines.size) + visualLines.removeAt(firstVisualIndex) + } + val splitLines = splitTextForWrapping(textualLine.text, getWidth()) + + visualLines.addAll(firstVisualIndex, splitLines.map { VisualLine(it, textualLineIndex) }) + textualLine.visualIndices = firstVisualIndex until firstVisualIndex + splitLines.size + } + + // TODO: This probably isn't necessary. Remove when feeling not lazy :) + protected open fun recalculateAllVisualLines() { + visualLines.clear() + + for ((index, textualLine) in textualLines.withIndex()) { + val splitLines = splitTextForWrapping(textualLine.text, getWidth()) + textualLine.visualIndices = visualLines.size..visualLines.size + splitLines.size + visualLines.addAll(splitLines.map { VisualLine(it, index) }) + } + } + + // TODO: Look into optimization of this algorithm + protected open fun splitTextForWrapping(text: String, maxLineWidth: Float): List { + return getStringSplitToWidth(text, maxLineWidth, getTextScale(), processColorCodes = false) + } + + protected fun commitTextRemoval(startPos: LinePosition, endPos: LinePosition, selectAfterUndo: Boolean) { + val removeTextOperation = RemoveTextOperation(startPos, endPos, selectAfterUndo) + commitTextOperation(removeTextOperation) + } + + private fun removeText(startPos: LinePosition, endPos: LinePosition) { + val textualStartPos = startPos.toTextualPos() + val textualEndPos = endPos.toTextualPos() + + val startTextualLine = textualLines[textualStartPos.line] + val endTextualLine = textualLines[textualEndPos.line] + + startTextualLine.text = startTextualLine.text.substring( + 0, + textualStartPos.column + ) + endTextualLine.text.substring(textualEndPos.column) + + val firstItemToDelete = textualStartPos.line + 1 + repeat(textualEndPos.line - firstItemToDelete + 1) { + textualLines.removeAt(firstItemToDelete) + } + + recalculateAllVisualLines() + + val heightDifference = getHeight() - visualLines.size * lineHeight + if (verticalScrollingOffset < heightDifference) + targetVerticalScrollingOffset = heightDifference.coerceAtMost(0f) + + updateAction(getText()) + textState.set(getText()) + } + + private fun setCursorPosition(newPosition: LinePosition) { + newPosition.toVisualPos().run { + cursor = this + otherSelectionEnd = this + cursorNeedsRefocus = true + } + } + + protected open fun getTextBetween(startPos: LinePosition, endPos: LinePosition): String { + val textStart = startPos.toTextualPos() + val textEnd = endPos.toTextualPos() + + return if (textStart.line == textEnd.line) { + textualLines[textStart.line].text.substring(textStart.column, textEnd.column) + } else { + val lines = mutableListOf() + lines.add(textualLines[textStart.line].text.substring(textStart.column)) + + for (i in textStart.line + 1 until textEnd.line) + lines.add(textualLines[i].text) + + lines.add(textualLines[textEnd.line].text.substring(0, textEnd.column)) + lines.joinToString("\n") + } + } + + protected open fun selectAll() { + cursor = LinePosition(0, 0, isVisual = true) + otherSelectionEnd = LinePosition(visualLines.size - 1, visualLines.last().length, isVisual = true) + } + + protected open fun hasSelection() = cursor != otherSelectionEnd + protected open fun selectionStart() = minOf(cursor, otherSelectionEnd) + protected open fun selectionEnd() = maxOf(cursor, otherSelectionEnd) + protected open fun getSelection() = selectionStart() to selectionEnd() + + protected open fun deleteSelection() { + if (!hasSelection()) + return + + commitTextRemoval(selectionStart(), selectionEnd(), selectAfterUndo = true) + } + + protected open fun copySelection() { + val (visualSelectionStart, visualSelectionEnd) = getSelection() + if (visualSelectionStart == visualSelectionEnd) + return + + UDesktop.setClipboardString(getTextBetween(visualSelectionStart, visualSelectionEnd)) + } + + protected open fun charBefore(pos: LinePosition) = pos.toTextualPos().let { + when { + it.isAtAbsoluteStart -> null + it.isAtLineStart -> '\n' + else -> textualLines[it.line].text[it.column - 1] + } + } + + protected open fun charAfter(pos: LinePosition) = pos.toTextualPos().let { + when { + it.isAtAbsoluteEnd -> null + it.isAtLineEnd -> '\n' + else -> textualLines[it.line].text[it.column] + } + } + + @ApiStatus.Internal + enum class Direction { + Left, + Right + } + + protected open fun isBreakingCharacter(ch: Char): Boolean { + return !ch.isLetterOrDigit() && ch != '_' + } + + protected open fun getNearestWordBoundary(pos: LinePosition, direction: Direction): LinePosition { + /* + * Algorithm: + * 1. If we can't go further in the specified direction, return pos + * 2. First, ignore all breaking characters until a non-breaking character is found + * or the beginning is reached + * 3. Consume until a breaking character is found or the beginning is reached + * 4. If our direction is left and we are at the end of a visual line, and we are not + * at the last line, return the position at the beginning of the next visual line + * 5. if our direction is right and we are at the beginning of a visual + * line, and we are not the first line, return the position at the end of the + * last visual line + * 6. Return the position + * + * Other conditions: + * - If a newline is encountered, one of the following actions happens: + * - If this is the first character, return the position past that newline + * - Otherwise, return the position before that newline + * - If our direction is left and we are at the end of a visual line, and we are not + * at the last line, return the position at the beginning of the next visual line + * - If our direction is right and we are at the beginning of a visual line, and we + * are not the first line, return the position at the end of the last visual line + */ + + // Step 1 + val atEndOfDirection = if (direction == Direction.Left) pos::isAtAbsoluteStart else pos::isAtAbsoluteEnd + if (atEndOfDirection()) + return pos + + var textualPos = pos.toTextualPos() + val columnOffset = if (direction == Direction.Left) -1 else 1 + val nextChar = if (direction == Direction.Left) ::charBefore else ::charAfter + + if (direction == Direction.Left && textualPos.isAtLineStart) { + val previousLine = textualLines[textualPos.line - 1] + return LinePosition(textualPos.line - 1, previousLine.length, isVisual = false) + } else if (direction == Direction.Right && textualPos.isAtLineEnd) { + return LinePosition(textualPos.line + 1, 0, isVisual = false) + } + + var ch = nextChar(textualPos) + + // Step 2 + while (!atEndOfDirection() && ch?.let(::isBreakingCharacter) == true) { + textualPos = textualPos.offsetColumn(columnOffset) + ch = nextChar(textualPos) + if (ch == '\n') + return textualPos + } + + // Step 3 + while (!atEndOfDirection() && ch?.let(::isBreakingCharacter) == false) { + textualPos = textualPos.offsetColumn(columnOffset) + ch = nextChar(textualPos) + if (ch == '\n') + return textualPos + } + + // Note that if we go into either of the if cases below, we will end up returning + // a visual position rather than a textual position. This is intentional, as a + // textual position cannot distinguish a visual end of line from a visual start + // of line (in other words, if you call `.toTextualPos()` on a visual EOL on line 5 + // and on a visual start of line on line 6, they will give you the same position) + // + // In order to distinguish this, if either of these are true, we have to return a + // visual position. Fortunately, because of the way we handle visual vs textual + // lines, this does not cause a problem + val visualPos = textualPos.toVisualPos() + if (direction == Direction.Left && visualPos.isAtLineEnd && !visualPos.isInLastLine) { // Step 4 + textualPos = LinePosition(visualPos.line + 1, 0, isVisual = true) + } else if (direction == Direction.Right && visualPos.isAtLineStart && !visualPos.isInFirstLine) { // Step 5 + textualPos = LinePosition(visualPos.line - 1, visualLines[visualPos.line - 1].text.length, isVisual = true) + } + + // Step 6 + return textualPos + } + + protected open fun animateCursor() { + if (!active) return + + cursorComponent.animate { + setColorAnimation(Animations.OUT_CIRCULAR, 0.5f, cursorColor.toConstraint()) + onComplete { + if (!active) return@onComplete + cursorComponent.animate { + setColorAnimation(Animations.IN_CIRCULAR, 0.5f, Color(255, 255, 255, 0).toConstraint()) + onComplete { + if (active) animateCursor() + } + } + } + } + } + + protected open fun hasText() = textualLines.size > 1 || textualLines[0].text.isNotEmpty() + + @Deprecated(UMatrixStack.Compat.DEPRECATED, ReplaceWith("drawUnselectedText(matrixStack, text, left, row)")) + protected open fun drawUnselectedText(text: String, left: Float, row: Int) = + drawUnselectedText(UMatrixStack.Compat.get(), text, left, row) + + @Suppress("DEPRECATION") + protected fun drawUnselectedTextCompat(matrixStack: UMatrixStack, text: String, left: Float, row: Int) = + UMatrixStack.Compat.runLegacyMethod(matrixStack) { drawUnselectedText(text, left, row) } + + protected open fun drawPlaceholder(matrixStack: UMatrixStack) { + drawUnselectedText(matrixStack, placeholder, getLeft(), 0, placeholderColor.get(), placeholderShadow.get()) + } + + protected open fun drawUnselectedText( + matrixStack: UMatrixStack, + text: String, + left: Float, + row: Int, + color: Color? = null, + shadow: Boolean = this.contentShadow, + shadowColor: Color? = this.contentShadowColor, + ) { + getFontProvider().drawString( + matrixStack, + text, + color ?: getColor(), + left - horizontalScrollingOffset, + getTop() + ((lineHeight * row + 1) * getTextScale()) + verticalScrollingOffset, + 10f, + getTextScale(), + shadow, + shadowColor, + ) + } + + @Deprecated(UMatrixStack.Compat.DEPRECATED, ReplaceWith("drawSelectedText(matrixStack, text, left, right, row)")) + protected open fun drawSelectedText(text: String, left: Float, right: Float, row: Int) = + drawSelectedText(UMatrixStack.Compat.get(), text, left, right, row) + + @Suppress("DEPRECATION") + protected fun drawSelectedTextCompat(matrixStack: UMatrixStack, text: String, left: Float, right: Float, row: Int) = + UMatrixStack.Compat.runLegacyMethod(matrixStack) { drawSelectedText(text, left, right, row) } + + protected open fun drawSelectedText(matrixStack: UMatrixStack, text: String, left: Float, right: Float, row: Int) { + UIBlock.drawBlock( + matrixStack, + if (active) selectionBackgroundColor else inactiveSelectionBackgroundColor, + left.toDouble() - horizontalScrollingOffset, + getTop().toDouble() + (lineHeight * row * getTextScale()) + verticalScrollingOffset, + right.toDouble() - horizontalScrollingOffset, + getTop().toDouble() + (lineHeight * ((row + 1) * getTextScale())) + verticalScrollingOffset + ) + if (text.isNotEmpty()) { + getFontProvider().drawString( + matrixStack, + text, + if (active) selectionForegroundColor else inactiveSelectionForegroundColor, + left - horizontalScrollingOffset, + getTop() + ((lineHeight * row + 1) * getTextScale()) + verticalScrollingOffset, + 10f, + getTextScale(), + contentShadow + ) + } + } + + override fun animationFrame() { + super.animationFrame() + + val diff = (targetVerticalScrollingOffset - verticalScrollingOffset) * 0.1f + if (abs(diff) < .25f) + verticalScrollingOffset = targetVerticalScrollingOffset + verticalScrollingOffset += diff + + recalculateDimensions() + + if (cursorNeedsRefocus) { + scrollIntoView(cursor) + cursorNeedsRefocus = false + } + } + + @ApiStatus.Internal + protected inner class LinePosition(val line: Int, val column: Int, val isVisual: Boolean) : + Comparable { + val isAtLineStart: Boolean get() = column == 0 + val isAtLineEnd: Boolean get() = column == lines[line].length + + val isInFirstLine: Boolean get() = line == 0 + val isInLastLine: Boolean get() = line == lines.lastIndex + + val isAtAbsoluteStart: Boolean get() = isInFirstLine && isAtLineStart + val isAtAbsoluteEnd: Boolean get() = isInLastLine && isAtLineEnd + + private val lines: List = if (isVisual) visualLines else textualLines + + fun offsetColumn(amount: Int) = when { + amount > 0 -> offsetColumnPositive(amount, this) + amount < 0 -> offsetColumnNegative(-amount, this) + else -> this + } + + private tailrec fun offsetColumnNegative(amount: Int, pos: LinePosition): LinePosition { + if (amount == 0 || pos.isAtAbsoluteStart) + return pos + + return offsetColumnNegative(amount - 1, complexOffsetColumnNegative(pos)) + } + + private fun complexOffsetColumnNegative(pos: LinePosition): LinePosition { + if (!pos.isVisual) + return simpleOffsetColumnNegative(pos) + if (!pos.isAtLineStart) + return simpleOffsetColumnNegative(pos) + + val currentLine = visualLines[pos.line] + val previousLine = visualLines[pos.line - 1] + if (currentLine.textIndex != previousLine.textIndex) + return simpleOffsetColumnNegative(pos) + if (previousLine.text.last() != ' ') + return simpleOffsetColumnNegative(pos) + return LinePosition(pos.line - 1, previousLine.length - 1, isVisual = true) + } + + private fun simpleOffsetColumnNegative(pos: LinePosition) = if (pos.column == 0) { + LinePosition(pos.line - 1, pos.lines[pos.line - 1].length, pos.isVisual) + } else { + pos.withColumn(pos.column - 1) + } + + private tailrec fun offsetColumnPositive(amount: Int, pos: LinePosition): LinePosition { + if (amount == 0 || pos.isAtAbsoluteEnd) + return pos + + return offsetColumnPositive(amount - 1, complexOffsetColumnPositive(pos)) + } + + private fun complexOffsetColumnPositive(pos: LinePosition): LinePosition { + if (!pos.isVisual) + return simpleOffsetColumnPositive(pos) + + val currentLine = visualLines[pos.line] + if (pos.column < currentLine.length - 1) + return simpleOffsetColumnPositive(pos) + if (pos.line == visualLines.lastIndex) + return LinePosition(pos.line, currentLine.length, isVisual = true) + if (pos.column == currentLine.length - 1 && currentLine.text.last() != ' ') + return simpleOffsetColumnPositive(pos) + + val nextLine = visualLines[pos.line + 1] + if (currentLine.textIndex == nextLine.textIndex) + return LinePosition(pos.line + 1, 0, isVisual = true) + return simpleOffsetColumnPositive(pos) + } + + private fun simpleOffsetColumnPositive(pos: LinePosition) = if (pos.column >= pos.lines[pos.line].length) { + if (pos.line == pos.lines.lastIndex) { + LinePosition(pos.lines.lastIndex, pos.lines.last().length, pos.isVisual) + } else { + LinePosition(pos.line + 1, 0, pos.isVisual) + } + } else { + pos.withColumn(pos.column + 1) + } + + fun withColumn(newColumn: Int) = LinePosition(line, newColumn, isVisual) + + fun toTextualPos(): LinePosition { + if (!isVisual) + return this + + val visualLine = visualLines[line] + val textualLine = textualLines[visualLine.textIndex] + var totalVisualLength = 0 + + for (i in textualLine.visualIndices.first until line) + totalVisualLength += visualLines[i].length + + return LinePosition(visualLine.textIndex, totalVisualLength + column, isVisual = false) + } + + fun toVisualPos(): LinePosition { + if (isVisual) + return this + + val textualLine = textualLines[line] + var lengthRemaining = column + + for (visualLineIndex in textualLine.visualIndices) { + val visualLine = visualLines[visualLineIndex] + if (visualLine.length >= lengthRemaining) + return LinePosition(visualLineIndex, lengthRemaining, isVisual = true) + + lengthRemaining -= visualLine.length + } + + println("toTextualPos: Unexpected end of function") + return LinePosition(0, 0, isVisual = true) + } + + fun toScreenPos(): Pair { + val visualPos = toVisualPos() + val x = visualLines[visualPos.line].text.substring(0, visualPos.column) + .width(getTextScale()) - horizontalScrollingOffset + val y = (lineHeight * visualPos.line * getTextScale()) + verticalScrollingOffset + return x to y + } + + override operator fun compareTo(other: LinePosition): Int { + val thisVisual = toVisualPos() + val otherVisual = other.toVisualPos() + + return when { + thisVisual.line < otherVisual.line -> -1 + thisVisual.line > otherVisual.line -> 1 + thisVisual.column < otherVisual.column -> -1 + thisVisual.column > otherVisual.column -> 1 + else -> 0 + } + } + + override fun equals(other: Any?) = + other is LinePosition && line == other.line && column == other.column && isVisual == other.isVisual + + override fun hashCode(): Int { + var result = line + result = 31 * result + column + result = 31 * result + isVisual.hashCode() + return result + } + + override fun toString() = "LinePosition(line=$line, column=$column, isVisual=$isVisual)" + } + + @ApiStatus.Internal + protected open inner class Line(var text: String) { + val length: Int get() = text.length + } + + @ApiStatus.Internal + protected inner class TextualLine(text: String, var visualIndices: IntRange = 0..0) : Line(text) { + fun addTextAt(newText: String, column: Int) { + if (column >= text.length) { + text += newText + } else { + text = text.substring(0, column) + newText + text.substring(column) + } + } + + override fun toString() = "TextualLine(text=$text, visualIndices=$visualIndices)" + } + + @ApiStatus.Internal + protected inner class VisualLine(text: String, val textIndex: Int) : Line(text) { + override fun toString() = "VisualLine(text=$text, textIndex=$textIndex)" + } + + @ApiStatus.Internal + protected abstract inner class TextOperation { + abstract fun redo() + abstract fun undo() + } + + @ApiStatus.Internal + protected inner class AddTextOperation(private val newText: String, private val startPos: LinePosition) : + TextOperation() { + override fun redo() { + addText(newText, startPos) + } + + override fun undo() { + removeText(startPos, startPos.offsetColumn(newText.length)) + setCursorPosition(startPos.toVisualPos()) + } + } + + @ApiStatus.Internal + protected inner class RemoveTextOperation( + private val startPos: LinePosition, private val endPos: LinePosition, private val selectAfterUndo: Boolean + ) : TextOperation() { + val text = getTextBetween(startPos, endPos) + + override fun redo() { + val textualStartPos = startPos.toTextualPos() + removeText(textualStartPos, endPos) + setCursorPosition(textualStartPos) + } + + override fun undo() { + addText(text, startPos) + if (selectAfterUndo) { + cursor = startPos + otherSelectionEnd = endPos + cursorNeedsRefocus = true + } + } + } + + @ApiStatus.Internal + protected inner class ReplaceTextOperation( + private val addTextOperation: AddTextOperation, + private val removeTextOperation: RemoveTextOperation + ) : TextOperation() { + override fun redo() { + removeTextOperation.redo() + addTextOperation.redo() + } + + override fun undo() { + addTextOperation.undo() + removeTextOperation.undo() + } + } +} diff --git a/src/main/kotlin/gg/essential/elementa/components/input/v2/UITextInput.kt b/src/main/kotlin/gg/essential/elementa/components/input/v2/UITextInput.kt new file mode 100644 index 00000000..849fcc2a --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/input/v2/UITextInput.kt @@ -0,0 +1,154 @@ +package gg.essential.elementa.components.input.v2 + +import gg.essential.elementa.constraints.WidthConstraint +import gg.essential.elementa.dsl.* +import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +open class UITextInput @JvmOverloads constructor( + placeholder: String = "", + shadow: Boolean = true, + shadowColor: Color? = null, + selectionBackgroundColor: Color = Color.WHITE, + selectionForegroundColor: Color = Color(64, 139, 229), + allowInactiveSelection: Boolean = false, + inactiveSelectionBackgroundColor: Color = Color(176, 176, 176), + inactiveSelectionForegroundColor: Color = Color.WHITE, + cursorColor: Color = Color.WHITE +) : AbstractTextInput( + placeholder, + shadow, + shadowColor, + selectionBackgroundColor, + selectionForegroundColor, + allowInactiveSelection, + inactiveSelectionBackgroundColor, + inactiveSelectionForegroundColor, + cursorColor = Color.WHITE +) { + protected var minWidth: WidthConstraint? = null + protected var maxWidth: WidthConstraint? = null + + protected val placeholderWidth = placeholder.width() + + fun setMinWidth(constraint: WidthConstraint) = apply { + minWidth = constraint + } + + fun setMaxWidth(constraint: WidthConstraint) = apply { + maxWidth = constraint + } + + override fun getText() = textualLines.first().text + + protected open fun getTextForRender(): String = getText() + + protected open fun setCursorPos() { + cursorComponent.unhide() + val (cursorPosX, _) = cursor.toScreenPos() + cursorComponent.setX((cursorPosX).pixels()) + } + + override fun textToLines(text: String): List { + return listOf(text.replace('\n', ' ')) + } + + override fun scrollIntoView(pos: LinePosition) { + val column = pos.column + val lineText = getTextForRender() + if (column < 0 || column > lineText.length) + return + + val widthBeforePosition = lineText.substring(0, column).width(getTextScale()) + + when { + getTextForRender().width(getTextScale()) < getWidth() -> { + horizontalScrollingOffset = 0f + } + horizontalScrollingOffset > widthBeforePosition -> { + horizontalScrollingOffset = widthBeforePosition + } + widthBeforePosition - horizontalScrollingOffset > getWidth() -> { + horizontalScrollingOffset = widthBeforePosition - getWidth() + } + } + } + + override fun screenPosToVisualPos(x: Float, y: Float): LinePosition { + val targetXPos = x + horizontalScrollingOffset + var currentX = 0f + + val line = getTextForRender() + + for (i in line.indices) { + val charWidth = line[i].width(getTextScale()) + if (currentX + (charWidth / 2) >= targetXPos) return LinePosition(0, i, isVisual = true) + currentX += charWidth + } + + return LinePosition(0, line.length, isVisual = true) + } + + override fun recalculateDimensions() { + if (minWidth != null && maxWidth != null) { + val width = if (!hasText() && !this.active) { + placeholderWidth + } else { + getTextForRender().width(getTextScale()) + 1 /* cursor */ + } + setWidth(width.pixels().coerceIn(minWidth!!, maxWidth!!)) + } + } + + override fun splitTextForWrapping(text: String, maxLineWidth: Float): List { + return listOf(text) + } + + override fun onEnterPressed() { + activateAction(getText()) + } + + override fun draw(matrixStack: UMatrixStack) { + beforeDrawCompat(matrixStack) + + if (!active && !hasText()) { + drawPlaceholder(matrixStack) + return super.draw(matrixStack) + } + + val lineText = getTextForRender() + + if (hasSelection()) { + var currentX = getLeft() + cursorComponent.hide(instantly = true) + + if (!selectionStart().isAtLineStart) { + val preSelectionText = lineText.substring(0, selectionStart().column) + drawUnselectedText(matrixStack, preSelectionText, currentX, row = 0) + currentX += preSelectionText.width(getTextScale()) + } + + val selectedText = lineText.substring(selectionStart().column, selectionEnd().column) + val selectedTextWidth = selectedText.width(getTextScale()) + drawSelectedText(matrixStack, selectedText, currentX, currentX + selectedTextWidth, row = 0) + currentX += selectedTextWidth + + if (!selectionEnd().isAtLineEnd) { + drawUnselectedText(matrixStack, lineText.substring(selectionEnd().column), currentX, row = 0) + } + } else { + if (active) { + cursorComponent.setY(basicYConstraint { + getTop() + }) + setCursorPos() + } + + drawUnselectedText(matrixStack, lineText, getLeft(), 0) + } + + super.draw(matrixStack) + } +} diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/CompactToggle.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/CompactToggle.kt new file mode 100644 index 00000000..00376e92 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/CompactToggle.kt @@ -0,0 +1,56 @@ +package gg.essential.elementa.components.inspector + +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.AspectConstraint +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.onLeftClick +import gg.essential.elementa.utils.onSetValueAndNow +import gg.essential.universal.USound +import java.awt.Color + +internal class CompactToggle( + private val enabled: State, +) : UIBlock() { + + private val switchBox by UIBlock().constrain { + width = AspectConstraint() + height = 100.percent + color = notchColor.toConstraint() + y = CenterConstraint() + } childOf this + + init { + onLeftClick { + USound.playButtonPress() + enabled.set { !it } + } + + constrain { + width = AspectConstraint(2f) + height = 7.pixels + } + + setColor(backgroundColor.toConstraint()) + + enabled.onSetValueAndNow { + val xConstraint = 0.pixels(alignOpposite = it) + // Null during init + if (Window.ofOrNull(this@CompactToggle) != null) { + switchBox.animate { + setXAnimation(Animations.OUT_EXP, 0.25f, xConstraint) + } + } else { + switchBox.setX(xConstraint) + } + } + } + companion object { + + internal val backgroundColor = Color(0xBBBBBB) + internal val notchColor = Color(0x2BC553) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlock.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlock.kt index 5d5b171e..0b538376 100644 --- a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlock.kt +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlock.kt @@ -1,309 +1,107 @@ package gg.essential.elementa.components.inspector import gg.essential.elementa.UIComponent -import gg.essential.elementa.UIConstraints -import gg.essential.elementa.components.TreeNode -import gg.essential.elementa.components.TreeListComponent import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.inspector.tabs.ConstraintsTab +import gg.essential.elementa.components.inspector.tabs.InspectorTab +import gg.essential.elementa.components.inspector.tabs.StatesTab +import gg.essential.elementa.components.inspector.tabs.ValuesTab import gg.essential.elementa.constraints.* -import gg.essential.elementa.constraints.animation.* -import gg.essential.elementa.constraints.debug.CycleSafeConstraintDebugger -import gg.essential.elementa.constraints.debug.withDebugger import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.hoveredState +import gg.essential.elementa.utils.onLeftClick +import gg.essential.elementa.utils.onSetValueAndNow +import gg.essential.elementa.utils.or import gg.essential.universal.UMatrixStack +import gg.essential.universal.USound +import org.jetbrains.annotations.ApiStatus import java.awt.Color class InfoBlock(private val inspector: Inspector) : UIContainer() { private var cachedComponent: UIComponent? = null - private val tabContainer = UIContainer().constrain { - width = ChildBasedSizeConstraint() + 15.pixels() - height = ChildBasedMaxSizeConstraint() + 10.pixels() + private val tabContainer by UIContainer().constrain { + width = ChildBasedSizeConstraint() + 15.pixels + height = ChildBasedMaxSizeConstraint() + 10.pixels } childOf this - private val contentContainer = UIContainer().constrain { + private val contentContainer by UIContainer().constrain { y = SiblingConstraint() - width = ChildBasedSizeConstraint() + width = ChildBasedSizeConstraint() + 10.pixels height = ChildBasedSizeConstraint() } childOf this - private val constraintsContent = TreeListComponent().constrain { - x = 5.pixels() - y = 5.pixels() - width = ChildBasedMaxSizeConstraint() - height = ChildBasedSizeConstraint() - } childOf contentContainer - - private val valuesContent = UIContainer().constrain { - x = 20.pixels() - y = 5.pixels() - width = ChildBasedMaxSizeConstraint() - height = ChildBasedSizeConstraint() - } + private val constraintsTab = ConstraintsTab() + private val selectedTab: State = BasicState(constraintsTab) - private lateinit var constraintsText: UIText - private lateinit var valuesText: UIText - - private val xValueText: UIText - private val yValueText: UIText - private val widthValueText: UIText - private val heightValueText: UIText - private val radiusValueText: UIText - private val textScaleValueText: UIText - private val colorValueText: UIText - - private var constraintsSelected = true - private val currentRoots = mutableMapOf() + private val tabs = listOf(constraintsTab, ValuesTab(), StatesTab()) init { - constraintsText = UIText("Constraints").constrain { - x = 5.pixels() - y = 5.pixels() - width = TextAspectConstraint() - color = Color.WHITE.toConstraint() - }.onMouseEnter { - if (!constraintsSelected) { - constraintsText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color.WHITE.toConstraint()) - } - } - }.onMouseLeave { - if (!constraintsSelected) { - constraintsText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color(255, 255, 255, 102).toConstraint()) - } - } - }.onMouseClick { - if (!constraintsSelected) { - constraintsSelected = true - contentContainer.removeChild(valuesContent) - contentContainer.addChild(constraintsContent) - valuesText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color(255, 255, 255, 102).toConstraint()) - } - } - } as UIText childOf tabContainer - - valuesText = UIText("Values").constrain { - x = SiblingConstraint(10f) - y = 5.pixels() - width = TextAspectConstraint() - color = Color(255, 255, 255, 102).toConstraint() - }.onMouseEnter { - if (constraintsSelected) { - valuesText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color.WHITE.toConstraint()) - } - } - }.onMouseLeave { - if (constraintsSelected) { - valuesText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color(255, 255, 255, 102).toConstraint()) - } - } - }.onMouseClick { - if (constraintsSelected) { - constraintsSelected = false - contentContainer.removeChild(constraintsContent) - contentContainer.addChild(valuesContent) - constraintsText.animate { - setColorAnimation(Animations.OUT_EXP, 0.5f, Color(255, 255, 255, 102).toConstraint()) + tabs.forEach { tab -> + UIText(tab.name).constrain { + y = CenterConstraint() + x = SiblingConstraint(5f) + }.apply { + setColor((hoveredState() or selectedTab.map { it == tab }).map { + if (it) { + Color.WHITE + } else { + Color(255, 255, 255, 102) + } + }.toConstraint()) + onLeftClick { + USound.playButtonPress() + selectedTab.set(tab) } - } - } as UIText childOf tabContainer - - xValueText = UIText("0") - yValueText = UIText("0") - widthValueText = UIText("0") - heightValueText = UIText("0") - radiusValueText = UIText("0") - textScaleValueText = UIText("0") - colorValueText = UIText("Color(255, 255, 255)") - - initializeText("x", xValueText) childOf valuesContent - initializeText("y", yValueText) childOf valuesContent - initializeText("width", widthValueText) childOf valuesContent - initializeText("height", heightValueText) childOf valuesContent - initializeText("radius", radiusValueText) childOf valuesContent - initializeText("textScale", textScaleValueText) childOf valuesContent - initializeText("color", colorValueText) childOf valuesContent - } - - private fun initializeText(name: String, valueText: UIText): UIContainer { - val container = UIContainer().constrain { - y = SiblingConstraint() - width = ChildBasedSizeConstraint() - height = ChildBasedMaxSizeConstraint() + 3.pixels() + } childOf tabContainer } - UIText("$name: ").constrain { - width = TextAspectConstraint() - } childOf container - - valueText.constrain { - x = SiblingConstraint() - width = TextAspectConstraint() - } childOf container + selectedTab.onSetValueAndNow { + contentContainer.clearChildren() + it childOf contentContainer + } - return container } - private fun setNewConstraints(constraints: UIConstraints) { - setConstraintNodes(constraints) - constraints.addObserver { _, arg -> - if (arg !is ConstraintType) - return@addObserver - val constraint = when (arg) { - ConstraintType.X -> constraints.x - ConstraintType.Y -> constraints.y - ConstraintType.WIDTH -> constraints.width - ConstraintType.HEIGHT -> constraints.height - ConstraintType.RADIUS -> constraints.radius - ConstraintType.COLOR -> constraints.color - ConstraintType.TEXT_SCALE -> constraints.textScale - ConstraintType.FONT_PROVIDER -> constraints.fontProvider - } + override fun draw(matrixStack: UMatrixStack) { + super.draw(matrixStack) - when (arg) { - ConstraintType.COLOR -> { - currentRoots[arg] = - if (constraint !is ConstantColorConstraint || constraint.color != Color.WHITE) getNodeFromConstraint( - constraint, - arg.prettyName - ) else null - } - ConstraintType.TEXT_SCALE -> { - currentRoots[ConstraintType.TEXT_SCALE] = - if (constraint !is PixelConstraint || constraint.value != 1f) getNodeFromConstraint( - constraint, - arg.prettyName - ) else null - } - else -> { - currentRoots[arg] = - if (constraint !is PixelConstraint || constraint.value != 0f) getNodeFromConstraint( - constraint, - arg.prettyName - ) else null + var cachedComponent = cachedComponent + if (cachedComponent != inspector.selectedNode?.targetComponent) { + cachedComponent = inspector.selectedNode?.targetComponent.also { + this.cachedComponent = it + } + if (cachedComponent != null) { + tabs.forEach { + it.newComponent(cachedComponent) } } - - constraintsContent.setRoots(currentRoots.values.filterNotNull()) - } - } - - private fun setConstraintNodes(constraints: UIConstraints) { - currentRoots.clear() - - listOf( - constraints.x to ConstraintType.X, - constraints.y to ConstraintType.Y, - constraints.width to ConstraintType.WIDTH, - constraints.height to ConstraintType.HEIGHT, - constraints.radius to ConstraintType.RADIUS - ).forEach { (constraint, type) -> - currentRoots[type] = - if (constraint !is PixelConstraint || constraint.value != 0f) getNodeFromConstraint( - constraint, - type.prettyName - ) else null - } - - constraints.textScale.also { - currentRoots[ConstraintType.TEXT_SCALE] = - if (it !is PixelConstraint || it.value != 1f) getNodeFromConstraint(it, "TextScale") else null } - - constraints.color.also { - currentRoots[ConstraintType.COLOR] = - if (it !is ConstantColorConstraint || it.color != Color.WHITE) getNodeFromConstraint( - it, - "Color" - ) else null + if (cachedComponent != null) { + tabs.forEach { + it.updateValues() + } } - constraintsContent.setRoots(currentRoots.values.filterNotNull()) } - private fun getNodeFromConstraint(constraint: SuperConstraint<*>, name: String? = null): TreeNode { - val baseInfoNode = InfoBlockNode(constraint, name) - - return when (constraint) { - is AdditiveConstraint -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.constraint1)) - add(getNodeFromConstraint(constraint.constraint2)) - } - is CoerceAtMostConstraint -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.constraint)) - add(getNodeFromConstraint(constraint.maxConstraint)) - } - is CoerceAtLeastConstraint -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.constraint)) - add(getNodeFromConstraint(constraint.minConstraint)) - } - is SubtractiveConstraint -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.constraint1)) - add(getNodeFromConstraint(constraint.constraint2)) - } - is XAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - is YAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - is WidthAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - is HeightAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - is RadiusAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - is ColorAnimationComponent -> baseInfoNode.withChildren { - add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) - add(getNodeFromConstraint(constraint.newConstraint, name = "To")) - } - else -> baseInfoNode - } + @ApiStatus.Internal + fun openConstraintsTab() { + selectedTab.set(tabs[0]) } - override fun draw(matrixStack: UMatrixStack) { - super.draw(matrixStack) - - if (cachedComponent != inspector.selectedNode?.targetComponent) { - cachedComponent = inspector.selectedNode?.targetComponent - cachedComponent?.let { - setNewConstraints(it.constraints) - it.addObserver { _, arg -> - if (arg is UIConstraints) { - setNewConstraints(arg) - } - } - } - } - - if (!constraintsSelected && cachedComponent != null) withDebugger(CycleSafeConstraintDebugger()) { - xValueText.setText("%.2f".format(cachedComponent!!.getLeft())) - yValueText.setText("%.2f".format(cachedComponent!!.getTop())) - widthValueText.setText("%.2f".format(cachedComponent!!.getWidth())) - heightValueText.setText("%.2f".format(cachedComponent!!.getHeight())) - radiusValueText.setText("%.2f".format(cachedComponent!!.getRadius())) - textScaleValueText.setText("%.2f".format(cachedComponent!!.getTextScale())) + @ApiStatus.Internal + fun openValuesTab() { + selectedTab.set(tabs[1]) + } - val color = cachedComponent!!.getColor() - colorValueText.setText(if (color.alpha == 255) { - "Color(%d, %d, %d)".format(color.red, color.green, color.blue) - } else { - "Color(%d, %d, %d, %d)".format(color.red, color.green, color.blue, color.alpha) - }) - } + @ApiStatus.Internal + fun openStatesTab() { + selectedTab.set(tabs[2]) } -} +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt index 80a245e7..0dfe2d42 100644 --- a/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/InfoBlockNode.kt @@ -4,12 +4,11 @@ import gg.essential.elementa.UIComponent import gg.essential.elementa.components.TreeNode import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.inspector.tabs.StatesTab import gg.essential.elementa.constraints.* import gg.essential.elementa.constraints.animation.AnimationComponent -import gg.essential.elementa.dsl.childOf -import gg.essential.elementa.dsl.constrain -import gg.essential.elementa.dsl.pixels -import gg.essential.elementa.dsl.plus +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.dsl.* import java.awt.Color class InfoBlockNode(private val constraint: SuperConstraint, private val name: String? = null) : TreeNode() { @@ -26,22 +25,10 @@ class InfoBlockNode(private val constraint: SuperConstraint, private val n x = SiblingConstraint() } childOf this - val properties = when (constraint) { - is AlphaAspectColorConstraint -> listOf(constraint::color, constraint::alpha) - is AspectConstraint -> listOf(constraint::value) - is ChildBasedSizeConstraint -> listOf(constraint::padding) - is ConstantColorConstraint -> listOf(constraint::color) - is CramSiblingConstraint -> listOf(constraint::padding) - is PixelConstraint -> listOf( - constraint::value, - constraint::alignOpposite, - constraint::alignOutside - ) - is RainbowColorConstraint -> listOf(constraint::alpha, constraint::speed) - is RelativeConstraint -> listOf(constraint::value) - is ScaledTextConstraint -> listOf(constraint::scale) - is SiblingConstraint -> listOf(constraint::padding, constraint::alignOpposite) - else -> listOf() + val states = if (constraint is StateRegistry) { + constraint.managedStates + } else { + emptyList() } fun toString(o: Any) = when (o) { @@ -59,19 +46,21 @@ class InfoBlockNode(private val constraint: SuperConstraint, private val n if (constraint is AnimationComponent<*>) { createStringComponent("§7Strategy: ${constraint.strategy}§r") - val percentComplete = constraint.elapsedFrames.toFloat() / (constraint.totalFrames + constraint.delayFrames) + val percentComplete = + constraint.elapsedFrames.toFloat() / (constraint.totalFrames + constraint.delayFrames) createStringComponent("§7Completion Percentage: ${Inspector.percentFormat.format(percentComplete)}§r") createStringComponent("§7Paused: ${constraint.animationPaused}§r") } - properties.forEach { - createStringComponent("§7${it.name}: ${toString(it.get())}§r") + states.forEach { managedState -> + StatesTab.createStateViewer(managedState, stringHolder) } } fun createStringComponent(text: String) { UIText(text).constrain { y = SiblingConstraint() + color = Color(0xAAAAAA).toConstraint() } childOf stringHolder } @@ -102,4 +91,4 @@ class InfoBlockNode(private val constraint: SuperConstraint, private val n else -> false } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/Inspector.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/Inspector.kt index 0c5bb876..555a6e0f 100644 --- a/src/main/kotlin/gg/essential/elementa/components/inspector/Inspector.kt +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/Inspector.kt @@ -3,27 +3,36 @@ package gg.essential.elementa.components.inspector import gg.essential.elementa.ElementaVersion import gg.essential.elementa.UIComponent import gg.essential.elementa.components.* +import gg.essential.elementa.components.inspector.display.awt.AwtInspectorDisplay +import gg.essential.elementa.components.inspector.display.glfw.GLFWDisplay import gg.essential.elementa.constraints.* import gg.essential.elementa.dsl.* import gg.essential.elementa.effects.OutlineEffect import gg.essential.elementa.effects.ScissorEffect -import gg.essential.elementa.utils.ObservableAddEvent -import gg.essential.elementa.utils.ObservableClearEvent -import gg.essential.elementa.utils.ObservableRemoveEvent -import gg.essential.elementa.utils.elementaDebug +import gg.essential.elementa.font.DefaultFonts +import gg.essential.elementa.impl.ExternalInspectorDisplay +import gg.essential.elementa.impl.Platform +import gg.essential.elementa.manager.ResolutionManager +import gg.essential.elementa.utils.* import gg.essential.universal.UGraphics +import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus import org.lwjgl.opengl.GL11 import java.awt.Color import java.text.NumberFormat class Inspector @JvmOverloads constructor( - rootComponent: UIComponent, + private val rootComponent: UIComponent, backgroundColor: Color = Color(40, 40, 40), outlineColor: Color = Color(20, 20, 20), outlineWidth: Float = 2f, - maxSectionHeight: HeightConstraint? = null + maxSectionHeight: HeightConstraint? = null, ) : UIContainer() { + + + private val inspectorContent by InspectorContent() + private val displayManager = DisplayManager() private val rootNode = componentToNode(rootComponent) private val treeBlock: UIContainer private var TreeListComponent: TreeListComponent @@ -38,6 +47,45 @@ class Inspector @JvmOverloads constructor( private val outlineEffect = OutlineEffect(outlineColor, outlineWidth, drawAfterChildren = true) private var isClickSelecting = false + private val drawObserver by Overlay() + + private var measuringDistance = false + + private var acceptingKeyboardInput = true + + private val keyTypeListener: UIComponent.(typedChar: Char, keyCode: Int) -> Unit = { _, keyCode -> + if (keyCode == UKeyboard.KEY_F12) { + acceptingKeyboardInput = !acceptingKeyboardInput + } + if (acceptingKeyboardInput) { + when (keyCode) { + UKeyboard.KEY_M -> { + measuringDistance = !measuringDistance + } + UKeyboard.KEY_S -> { + openComponentSelector() + } + UKeyboard.KEY_C -> { + infoBlock.openConstraintsTab() + } + UKeyboard.KEY_V -> { + infoBlock.openValuesTab() + } + UKeyboard.KEY_B -> { + infoBlock.openStatesTab() + } + UKeyboard.KEY_D -> { + elementaDebug = !elementaDebug + } + } + } + } + + private val infoBlock by InfoBlock(this).constrain { + y = SiblingConstraint() + width = ChildBasedMaxSizeConstraint() + 10.pixels() + height = ChildBasedSizeConstraint() + 10.pixels() + } init { constrain { @@ -48,7 +96,7 @@ class Inspector @JvmOverloads constructor( container = UIBlock(backgroundColor).constrain { width = ChildBasedMaxSizeConstraint() height = ChildBasedSizeConstraint() - } effect outlineEffect childOf this + } effect outlineEffect childOf inspectorContent val titleBlock = UIContainer().constrain { x = CenterConstraint() @@ -68,9 +116,9 @@ class Inspector @JvmOverloads constructor( return@onMouseDrag if (button == 0) { - this@Inspector.constrain { - x = (this@Inspector.getLeft() + mouseX - clickPos!!.first).pixels() - y = (this@Inspector.getTop() + mouseY - clickPos!!.second).pixels() + inspectorContent.constrain { + x = (inspectorContent.getLeft() + mouseX - clickPos!!.first).pixels() + y = (inspectorContent.getTop() + mouseY - clickPos!!.second).pixels() } } } childOf container @@ -89,19 +137,7 @@ class Inspector @JvmOverloads constructor( height = RelativeConstraint(1f).to(title) as HeightConstraint }.onMouseClick { event -> event.stopPropagation() - isClickSelecting = true - - val rootWindow = Window.of(this) - rootWindow.clickInterceptor = { mouseX, mouseY, _ -> - rootWindow.clickInterceptor = null - isClickSelecting = false - - val targetComponent = getClickSelectTarget(mouseX.toFloat(), mouseY.toFloat()) - if (targetComponent != null) { - findAndSelect(targetComponent) - } - true - } + openComponentSelector() } childOf titleBlock separator1 = UIBlock(outlineColor).constrain { @@ -133,11 +169,6 @@ class Inspector @JvmOverloads constructor( height = 2.pixels() } - val infoBlock = InfoBlock(this).constrain { - y = SiblingConstraint() - width = ChildBasedMaxSizeConstraint() + 10.pixels() - height = ChildBasedSizeConstraint() + 10.pixels() - } infoBlockScroller = ScrollComponent().constrain { y = SiblingConstraint() @@ -147,6 +178,26 @@ class Inspector @JvmOverloads constructor( } infoBlock childOf infoBlockScroller + + drawObserver childOf rootComponent + + rootComponent.onKeyType(keyTypeListener) + } + + private fun openComponentSelector() { + isClickSelecting = true + + val rootWindow = Window.of(rootComponent) + rootWindow.clickInterceptor = { mouseX, mouseY, _ -> + rootWindow.clickInterceptor = null + isClickSelecting = false + + val targetComponent = getClickSelectTarget(mouseX.toFloat(), mouseY.toFloat()) + if (targetComponent != null) { + findAndSelect(targetComponent) + } + true + } } private fun componentToNode(component: UIComponent): InspectorNode { @@ -188,6 +239,15 @@ class Inspector @JvmOverloads constructor( return node } + private fun cleanup() { + drawObserver.hide(instantly = true) + rootComponent.keyTypedListeners.remove(keyTypeListener) + if (this in parent.children) { + parent.children.remove(this) + } + displayManager.cleanup() + } + internal fun setSelectedNode(node: InspectorNode?) { if (node == null) { container.removeChild(separator2) @@ -219,7 +279,8 @@ class Inspector @JvmOverloads constructor( val parentNode = findNodeAndExpandParents(component.parent) ?: return null val parentDisplay = parentNode.displayComponent parentDisplay.opened = true - return parentDisplay.childNodes.filterIsInstance().find { it.targetComponent == component } + return parentDisplay.childNodes + .filterIsInstance().find { it.targetComponent == component } } val node = findNodeAndExpandParents(component) ?: return @@ -233,62 +294,17 @@ class Inspector @JvmOverloads constructor( override fun animationFrame() { super.animationFrame() - // Make sure we are the top-most component (last to draw and first to receive input) Window.enqueueRenderOperation { - setFloating(false) - if (isMounted()) { // only if we are still mounted - setFloating(true) - } + ensureLastComponent(inspectorContent) } + } override fun draw(matrixStack: UMatrixStack) { - // If we got removed from our parent, we need to un-float ourselves - if (!isMounted()) { - Window.enqueueRenderOperation { setFloating(false) } - return - } - separator1.setWidth(container.getWidth().pixels()) separator2.setWidth(container.getWidth().pixels()) - if (isClickSelecting) { - val (mouseX, mouseY) = getMousePosition() - getClickSelectTarget(mouseX, mouseY) - } else { - selectedNode?.targetComponent - }?.also { component -> - val scissors = generateSequence(component) { if (it.parent != it) it.parent else null } - .flatMap { it.effects.filterIsInstance().asReversed() } - .toList() - .reversed() - - val x1 = component.getLeft().toDouble() - val y1 = component.getTop().toDouble() - val x2 = component.getRight().toDouble() - val y2 = component.getBottom().toDouble() - - // Clear the depth buffer cause we will be using it to draw our outside-of-scissor-bounds block - UGraphics.glClear(GL11.GL_DEPTH_BUFFER_BIT) - - // Draw a highlight on the element respecting its scissor effects - scissors.forEach { it.beforeDraw(matrixStack) } - UIBlock.drawBlock(matrixStack, Color(129, 212, 250, 100), x1, y1, x2, y2) - scissors.asReversed().forEach { it.afterDraw(matrixStack) } - - // Then draw another highlight (with depth testing such that we do not overwrite the previous one) - // which does not respect the scissor effects and thereby indicates where the element is drawn outside of - // its scissor bounds. - UGraphics.enableDepth() - UGraphics.depthFunc(GL11.GL_LESS) - ElementaVersion.v0.enableFor { // need the custom depth testing - UIBlock.drawBlock(matrixStack, Color(255, 100, 100, 100), x1, y1, x2, y2) - } - UGraphics.depthFunc(GL11.GL_LEQUAL) - UGraphics.disableDepth() - } - val debugState = elementaDebug elementaDebug = false try { @@ -298,7 +314,373 @@ class Inspector @JvmOverloads constructor( } } + @ApiStatus.Internal + inner class Overlay : UIComponent() { + override fun draw(matrixStack: UMatrixStack) { + // If we got removed from our parent, we need to un-float ourselves + if (!isMounted()) { + Window.enqueueRenderOperation { setFloating(false) } + cleanup() + return + } + beforeDraw(matrixStack) + val targetComponent = selectedNode?.targetComponent + if (isClickSelecting) { + val (mouseX, mouseY) = getMousePosition() + getClickSelectTarget(mouseX, mouseY) + } else { + targetComponent + }?.also { component -> + val scissors = generateSequence(component) { if (it.parent != it) it.parent else null } + .flatMap { it.effects.filterIsInstance().asReversed() } + .toList() + .reversed() + + val x1 = component.getLeft().toDouble() + val y1 = component.getTop().toDouble() + val x2 = component.getRight().toDouble() + val y2 = component.getBottom().toDouble() + + // Clear the depth buffer cause we will be using it to draw our outside-of-scissor-bounds block + UGraphics.glClear(GL11.GL_DEPTH_BUFFER_BIT) + + // Draw a highlight on the element respecting its scissor effects + scissors.forEach { it.beforeDraw(matrixStack) } + UIBlock.drawBlock(matrixStack, Color(129, 212, 250, 100), x1, y1, x2, y2) + scissors.asReversed().forEach { it.afterDraw(matrixStack) } + + // Then draw another highlight (with depth testing such that we do not overwrite the previous one) + // which does not respect the scissor effects and thereby indicates where the element is drawn outside of + // its scissor bounds. + UGraphics.enableDepth() + UGraphics.depthFunc(GL11.GL_LESS) + ElementaVersion.V1.enableFor { // need the custom depth testing + UIBlock.drawBlock(matrixStack, Color(255, 255, 255, 100), x1, y1, x2, y2) + } + UGraphics.depthFunc(GL11.GL_LEQUAL) + UGraphics.disableDepth() + } + + if (targetComponent != null && measuringDistance) { + val (mouseX, mouseY) = getMousePosition() + val hoveredComponent = getClickSelectTarget(mouseX, mouseY) + + + if (hoveredComponent != null && hoveredComponent != targetComponent) { + // Outline the hovered item + UIBlock.drawBlock( + matrixStack, + Color(255, 100, 100, 100), + hoveredComponent.getLeft().toDouble(), + hoveredComponent.getTop().toDouble(), + hoveredComponent.getRight().toDouble(), + hoveredComponent.getBottom().toDouble() + ) + + val horizontalLineY = + if (targetComponent.centerY() in hoveredComponent.getTop()..hoveredComponent.getBottom()) { + targetComponent.centerY() + } else if (targetComponent.getTop() > hoveredComponent.getBottom()) { + targetComponent.getTop() + } else { + targetComponent.getBottom() + } + + if (hoveredComponent.getRight() < targetComponent.getLeft()) { + measureHorizontalDistance( + matrixStack, + hoveredComponent.getRight(), + targetComponent.getLeft(), + horizontalLineY + ) + } else if (hoveredComponent.getRight() < targetComponent.getRight()) { + measureHorizontalDistance( + matrixStack, + hoveredComponent.getRight(), + targetComponent.getRight(), + horizontalLineY + ) + } + + if (hoveredComponent.getLeft() > targetComponent.getRight()) { + measureHorizontalDistance( + matrixStack, + targetComponent.getRight(), + hoveredComponent.getLeft(), + horizontalLineY + ) + } else if (hoveredComponent.getLeft() > targetComponent.getLeft()) { + measureHorizontalDistance( + matrixStack, + targetComponent.getLeft(), + hoveredComponent.getLeft(), + horizontalLineY + ) + } + + + val verticalLineX = + if (targetComponent.centerX() in hoveredComponent.getLeft()..hoveredComponent.getRight()) { + targetComponent.centerX() + } else if (hoveredComponent.getRight() < targetComponent.getLeft()) { + hoveredComponent.getRight() + } else { + hoveredComponent.getLeft() + } + if (hoveredComponent.getBottom() < targetComponent.getTop()) { + measureVerticalDistance( + matrixStack, + hoveredComponent.getBottom(), + targetComponent.getTop(), + verticalLineX + ) + } else if (hoveredComponent.getBottom() < targetComponent.getBottom()) { + measureVerticalDistance( + matrixStack, + hoveredComponent.getBottom(), + targetComponent.getBottom(), + verticalLineX + ) + } + + if (hoveredComponent.getTop() > targetComponent.getBottom()) { + measureVerticalDistance( + matrixStack, + targetComponent.getBottom(), + hoveredComponent.getTop(), + verticalLineX + ) + } else if (hoveredComponent.getTop() > targetComponent.getTop()) { + measureVerticalDistance( + matrixStack, + targetComponent.getTop(), + hoveredComponent.getTop(), + verticalLineX + ) + } + + + } + } + super.draw(matrixStack) + } + + override fun animationFrame() { + super.animationFrame() + // Make sure we are the top-most component (last to draw and first to receive input) + Window.enqueueRenderOperation { + ensureLastComponent(this@Overlay) + } + } + + fun UIComponent.centerX(): Float { + return (getLeft() + getRight()) / 2 + } + + fun UIComponent.centerY(): Float { + return (getTop() + getBottom()) / 2 + } + + fun drawShadedText( + matrixStack: UMatrixStack, + text: String, + xCenter: Float, + yCenter: Float, + ) { + val stringWidth = text.width() + val stringHeight = UGraphics.getFontHeight() + + + val x1 = (xCenter - stringWidth / 2.0).roundToRealPixels() + val y1 = (yCenter - stringHeight / 2.0).roundToRealPixels() + val x2 = (xCenter + stringWidth / 2.0).roundToRealPixels() + val y2 = (yCenter + stringHeight / 2.0).roundToRealPixels() + + // Draw outline for increased visiblity + UIBlock.drawBlock( + matrixStack, + Color.MAGENTA, + x1 - 1, + y1 - 1, + x2 + 1, + y2 + 1 + ) + + + DefaultFonts.VANILLA_FONT_RENDERER.drawString( + matrixStack, + text, + Color.WHITE, + x1.toFloat(), + y2.toFloat(), + 10f, + 1f, + false, + ) + } + + fun drawDistanceText( + matrixStack: UMatrixStack, + distance: Float, + xCenter: Float, + yCenter: Float, + ) { + drawShadedText( + matrixStack, + "${"%.2f".format(null, distance).trimEnd('0').trimEnd('.')}px", + xCenter, + yCenter, + ) + } + + fun measureVerticalDistance( + matrixStack: UMatrixStack, + y1: Float, + y2: Float, + x: Float, + ) { + val distance = y2 - y1 + if (distance > 0) { + UIBlock.drawBlock( + matrixStack, + Color.YELLOW, + (x - 1).toDouble(), + (y1).toDouble(), + (x + 1).toDouble(), + (y2).toDouble() + ) + drawDistanceText( + matrixStack, + distance, + x, + y1 + distance / 2, + ) + } + } + + fun measureHorizontalDistance( + matrixStack: UMatrixStack, + x1: Float, + x2: Float, + y: Float, + ) { + val distance = x2 - x1 + if (distance > 0) { + UIBlock.drawBlock( + matrixStack, + Color.YELLOW, + x1.toDouble(), + (y - 1).toDouble(), + x2.toDouble(), + (y + 1).toDouble() + ) + drawDistanceText( + matrixStack, + distance, + x1 + distance / 2, + y + 10, + ) + } + } + } + + private fun ensureLastComponent(component: UIComponent) { + val componentOrder = listOf( + Overlay::class.java, + InspectorContent::class.java, + Inspector::class.java, + ) + component.setFloating(false) + if (component.isMounted()) { // only if we are still mounted + component.setFloating(true) + val siblings = component.parent.children + siblings.sortBy { + componentOrder.indexOf(it.javaClass) + } + } + } + + fun setDetached(external: Boolean) { + displayManager.setDetached(external) + } + + // Class created so that we can reliability detect if a container is this type + internal inner class InspectorContent : UIContainer() { + + init { + constrain { + width = ChildBasedSizeConstraint() + height = ChildBasedSizeConstraint() + } + } + } + + internal inner class DisplayManager { + + private var externalInspectorDisplay: ExternalInspectorDisplay? = null + + // Create separate constraints so that the position of the inspector in the overlay + // can be different from it in the external window + private val externalContentConstraints = inspectorContent.constraints.copy() + private val overlayContentConstraints = inspectorContent.constraints.copy() + private var detached = !startDetached // Default to opposite state of startDetached so setDetached will run + private val drawCallback: Window.() -> Unit = { + draw(resolutionManager) + } + + init { + setDetached(startDetached) + Window.of(rootComponent).drawCallbacks.add(drawCallback) + } + + fun cleanup() { + externalInspectorDisplay?.cleanup() + Window.of(rootComponent).drawCallbacks.remove(drawCallback) + } + + fun draw(resolutionManager: ResolutionManager) { + externalInspectorDisplay?.updateFrameBuffer(resolutionManager) + } + + fun setDetached(detached: Boolean) { + if (detached == this.detached) { + return + } + this.detached = detached + val window = Window.of(rootComponent) + if (detached) { + var externalInspectorDisplay = externalInspectorDisplay + if (externalInspectorDisplay == null) { + externalInspectorDisplay = createExternalDisplay().also { + this.externalInspectorDisplay = it + } + } + inspectorContent.constraints = externalContentConstraints + + externalInspectorDisplay.addComponent(inspectorContent) + window.removeChild(inspectorContent) + window.removeFloatingComponent(inspectorContent) + } else { + externalInspectorDisplay?.removeComponent(inspectorContent) + window.addChild(inspectorContent) + inspectorContent.constraints = overlayContentConstraints + } + } + + private fun createExternalDisplay(): ExternalInspectorDisplay { + return when (Platform.platform.mcVersion) { + 11202, 10809 -> AwtInspectorDisplay() + else -> GLFWDisplay() + } + } + } + companion object { internal val percentFormat: NumberFormat = NumberFormat.getPercentInstance() + + /** + * Controls whether new inspectors or debug components open in a detached window or on the window they are debugging. + */ + internal var startDetached = System.getProperty("elementa.inspector.detached", "true") == "true" } -} +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtEventListenerAdaptor.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtEventListenerAdaptor.kt new file mode 100644 index 00000000..75ff8a2d --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtEventListenerAdaptor.kt @@ -0,0 +1,208 @@ +package gg.essential.elementa.components.inspector.display.awt + +import gg.essential.elementa.components.Window +import gg.essential.elementa.debug.ExternalResolutionManager +import gg.essential.elementa.impl.Platform +import gg.essential.elementa.manager.KeyboardManager +import gg.essential.universal.UKeyboard +import org.jetbrains.annotations.ApiStatus +import java.awt.event.* + +/** + * A [KeyboardManager] implementation that receives its values from a Java AWT + */ +internal class AwtEventListenerAdaptor( + private val resolutionManager: ExternalResolutionManager, + private val window: Window, +) : MouseListener, KeyListener, MouseWheelListener, KeyboardManager { + + private val currentlyPressedKeys = mutableSetOf() + private var modifiers = UKeyboard.Modifiers(isCtrl = false, isShift = false, isAlt = false) + + private fun runOnMinecraftThread(runnable: () -> Unit) { + Platform.platform.runOnMinecraftThread(runnable) + } + + override fun mouseReleased(e: MouseEvent) = runOnMinecraftThread { window.mouseRelease() } + + override fun keyPressed(e: KeyEvent) = runOnMinecraftThread { + awtToGlKeyMap[e.keyCode]?.let { keyCode -> + currentlyPressedKeys.add(keyCode) + modifiers = UKeyboard.Modifiers( + isCtrl = e.isControlDown, + isShift = e.isShiftDown, + isAlt = e.isAltDown + ) + if (keyCode == UKeyboard.KEY_UP) { + resolutionManager.scaleFactor++ + } else if (keyCode == UKeyboard.KEY_DOWN && resolutionManager.scaleFactor > 1) { + resolutionManager.scaleFactor++ + } else { + window.keyType(if (e.keyChar.code == 65535) 0.toChar() else e.keyChar, keyCode) + } + } + } + + override fun mouseWheelMoved(e: MouseWheelEvent) = runOnMinecraftThread { + window.mouseScroll(-e.preciseWheelRotation) + } + + override fun keyReleased(e: KeyEvent) = runOnMinecraftThread { + currentlyPressedKeys.remove(awtToGlKeyMap[e.keyCode] ?: return@runOnMinecraftThread) + } + + override fun isKeyDown(key: Int): Boolean { + return key in currentlyPressedKeys + } + + override fun allowRepeatEvents(enabled: Boolean) {} + + override fun getModifiers(): UKeyboard.Modifiers { + return modifiers + } + + override fun mousePressed(e: MouseEvent) = runOnMinecraftThread { + // Convert from Java AWT buttons to LWJGL buttons + val mappedButton = when (e.button) { + MouseEvent.BUTTON1 -> 0 + MouseEvent.BUTTON2 -> 2 + MouseEvent.BUTTON3 -> 1 + else -> return@runOnMinecraftThread + } + val mouseX = e.x.toDouble() / resolutionManager.scaleFactor + val mouseY = e.y.toDouble() / resolutionManager.scaleFactor + window.mouseClick(mouseX, mouseY, mappedButton) + } + + /** Not needed **/ + override fun mouseClicked(e: MouseEvent) {} + + override fun keyTyped(e: KeyEvent) {} + + override fun mouseEntered(e: MouseEvent) {} + + override fun mouseExited(e: MouseEvent) {} + + @ApiStatus.Internal + companion object { + + // Adapted from https://stackoverflow.com/questions/26617817/converting-between-different-keycodes + private val awtToGlKeyMap = mapOf( + 8 to 14, + 32 to 57, + 9 to 15, + 13 to 28, + 10 to 28, + 16 to 42, + 17 to 29, + 18 to 56, + 19 to 197, + 20 to 58, + 27 to 1, + 33 to 201, + 34 to 209, + 35 to 207, + 36 to 199, + 37 to 203, + 38 to 200, + 39 to 205, + 40 to 208, + 155 to 210, + 127 to 211, + 48 to 11, + 49 to 2, + 50 to 3, + 51 to 4, + 5 to 5, + 53 to 6, + 54 to 7, + 55 to 8, + 56 to 9, + 57 to 10, + 65 to 30, + 66 to 48, + 67 to 46, + 68 to 32, + 69 to 18, + 70 to 33, + 71 to 34, + 72 to 35, + 73 to 23, + 74 to 36, + 75 to 37, + 76 to 38, + 77 to 50, + 78 to 49, + 79 to 24, + 80 to 25, + 81 to 16, + 82 to 19, + 83 to 31, + 84 to 20, + 85 to 22, + 86 to 47, + 87 to 17, + 88 to 45, + 89 to 21, + 90 to 44, + 16777413 to 27, + 16777412 to 40, + 16777430 to 41, + 91 to 219, + 92 to 220, + 524 to 219, + 93 to 221, + 96 to 82, + 97 to 79, + 98 to 80, + 99 to 81, + 100 to 75, + 101 to 76, + 102 to 77, + 103 to 71, + 104 to 72, + 105 to 73, + 106 to 55, + 107 to 78, + 109 to 74, + 110 to 83, + 111 to 181, + 112 to 59, + 113 to 60, + 114 to 61, + 115 to 62, + 116 to 63, + 117 to 64, + 118 to 65, + 119 to 66, + 120 to 67, + 121 to 68, + 122 to 87, + 123 to 88, + 124 to 100, + 125 to 101, + 126 to 102, + 144 to 69, + 145 to 70, + 186 to 39, + 187 to 13, + 188 to 51, + 44 to 51, + 189 to 12, + 190 to 52, + 46 to 52, + 191 to 53, + 192 to 41, + 219 to 26, + 220 to 43, + 221 to 27, + 222 to 40, + 16777383 to 43, + 521 to 13, + 45 to 12, + 135 to 144, + ) + } + + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtFrameBufferCanvas.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtFrameBufferCanvas.kt new file mode 100644 index 00000000..af4d4f58 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtFrameBufferCanvas.kt @@ -0,0 +1,40 @@ +package gg.essential.elementa.components.inspector.display.awt + +import gg.essential.elementa.debug.FrameBufferedWindow +import org.lwjgl.opengl.AWTGLCanvas +import org.lwjgl.opengl.Display +import org.lwjgl.opengl.GL11.* +import org.lwjgl.opengl.PixelFormat +import org.lwjgl.opengl.SharedDrawable +import org.lwjgl.util.glu.GLU.gluOrtho2D +import java.awt.GraphicsEnvironment + +/** + * A Java AWT component that renders the contents of the supplied [bufferedWindow] + */ +internal class AwtFrameBufferCanvas( + private val bufferedWindow: FrameBufferedWindow, +) : AWTGLCanvas( + GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice, + PixelFormat(), + SharedDrawable(Display.getDrawable()), +) { + + + override fun paintGL() { + glViewport(0, 0, width, height) + glClear(GL_COLOR_BUFFER_BIT) + glMatrixMode(GL_PROJECTION) + glLoadIdentity() + gluOrtho2D(0.0f, width.toFloat(), height.toFloat(), 0.0f) + glMatrixMode(GL_MODELVIEW) + glPushMatrix() + glColor3f(1f, 1f, 1f) + + bufferedWindow.renderFrameBufferTexture() + + glPopMatrix() + swapBuffers() + repaint() + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtInspectorDisplay.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtInspectorDisplay.kt new file mode 100644 index 00000000..b6f4b7ea --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtInspectorDisplay.kt @@ -0,0 +1,83 @@ +package gg.essential.elementa.components.inspector.display.awt + +import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window +import gg.essential.elementa.debug.ExternalResolutionManager +import gg.essential.elementa.debug.FrameBufferedWindow +import gg.essential.elementa.impl.ExternalInspectorDisplay +import gg.essential.elementa.manager.ResolutionManager +import java.awt.Dimension +import java.util.concurrent.Executors +import javax.swing.JFrame +import javax.swing.WindowConstants + + +/** + * Implementation for [ExternalInspectorDisplay] using Java AWT for LWJGL2 + */ +internal class AwtInspectorDisplay : ExternalInspectorDisplay { + + private val window = Window(ElementaVersion.V2) + private val buffer = FrameBufferedWindow(window, this) + private val frame = JFrame("Inspector") + private val canvas = AwtFrameBufferCanvas(buffer) + private val resolutionManager = ExternalResolutionManager(this) + private val threadPool = Executors.newFixedThreadPool(1) + + private val listenerAdaptor = AwtEventListenerAdaptor(resolutionManager, window) + + init { + window.resolutionManager = resolutionManager + window.keyboardManager = listenerAdaptor + frame.defaultCloseOperation = WindowConstants.HIDE_ON_CLOSE + frame.add(canvas) + val height = 480 + canvas.preferredSize = Dimension(height * 16 / 9, height) + frame.pack() + window.mousePositionManager = AwtMousePositionManager(canvas, resolutionManager) + canvas.addMouseListener(listenerAdaptor) + canvas.addMouseWheelListener(listenerAdaptor) + canvas.addKeyListener(listenerAdaptor) + } + + override val visible: Boolean + get() = frame.isVisible + + override fun updateVisiblity(visible: Boolean) = threadPool.execute { + frame.isVisible = visible + } + + override fun addComponent(component: UIComponent) { + if (!visible) { + updateVisiblity(true) + } + window.addChild(component) + } + + override fun removeComponent(component: UIComponent) { + window.removeChild(component) + if (window.children.isEmpty()) { + updateVisiblity(false) + } + } + + override fun getWidth(): Int { + return canvas.width + } + + override fun getHeight(): Int { + return canvas.height + } + + override fun updateFrameBuffer(resolutionManager: ResolutionManager) { + buffer.updateFrameBuffer(resolutionManager) + } + + override fun cleanup() { + frame.dispose() + threadPool.shutdownNow() + buffer.deleteFrameBuffer() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtMousePositionManager.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtMousePositionManager.kt new file mode 100644 index 00000000..3e1e894f --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/awt/AwtMousePositionManager.kt @@ -0,0 +1,39 @@ +package gg.essential.elementa.components.inspector.display.awt + +import gg.essential.elementa.manager.MousePositionManager +import gg.essential.elementa.manager.ResolutionManager +import java.awt.event.MouseEvent +import java.awt.event.MouseMotionListener + +/** + * A [MousePositionManager] that supplies its values from the supplied [frame] + */ +internal class AwtMousePositionManager( + private val frame: AwtFrameBufferCanvas, + private val resolutionManager: ResolutionManager, +) : MousePositionManager, MouseMotionListener { + + init { + frame.addMouseMotionListener(this) + } + + override var rawX: Double = -1.0 + + override var rawY: Double = -1.0 + + override val scaledX: Double + get() = rawX / resolutionManager.scaleFactor + + override val scaledY: Double + get() = rawY / resolutionManager.scaleFactor + + override fun mouseDragged(e: MouseEvent) { + rawX = e.x.toDouble() + rawY = e.y.toDouble() + } + + override fun mouseMoved(e: MouseEvent) { + rawX = e.x.toDouble() + rawY = e.y.toDouble() + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWDisplay.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWDisplay.kt new file mode 100644 index 00000000..d7200d85 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWDisplay.kt @@ -0,0 +1,80 @@ +package gg.essential.elementa.components.inspector.display.glfw + +import gg.essential.elementa.ElementaVersion +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window +import gg.essential.elementa.debug.ExternalResolutionManager +import gg.essential.elementa.debug.FrameBufferedWindow +import gg.essential.elementa.impl.ExternalInspectorDisplay +import gg.essential.elementa.manager.ResolutionManager +import org.lwjgl.glfw.GLFW.* + +internal class GLFWDisplay : ExternalInspectorDisplay { + + private val window = Window(ElementaVersion.V2) + private val buffer = FrameBufferedWindow(window, this) + private val windowPointer = try { + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE) + glfwCreateWindow(852, 480, "Inspector", 0, glfwGetCurrentContext()) + } finally { + glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE) + } + private val renderer = GLFWRenderer(buffer, windowPointer, this) + private val resolutionManager = ExternalResolutionManager(this) + private val inputReceiver = GLFWInputReceiver(windowPointer, resolutionManager, window) + + init { + window.mousePositionManager = inputReceiver + window.keyboardManager = inputReceiver + window.resolutionManager = resolutionManager + } + + override var visible: Boolean = false + + override fun updateVisiblity(visible: Boolean) { + this.visible = visible + if (visible) { + glfwShowWindow(windowPointer) + } else { + glfwHideWindow(windowPointer) + } + } + + override fun addComponent(component: UIComponent) { + if (!visible) { + updateVisiblity(true) + } + window.addChild(component) + } + + override fun removeComponent(component: UIComponent) { + window.removeChild(component) + if (window.children.isEmpty()) { + updateVisiblity(false) + } + } + + override fun getWidth(): Int { + return getSize().first + } + + private fun getSize(): Pair { + val widthArray = IntArray(1) + val heightArray = IntArray(1) + glfwGetWindowSize(windowPointer, widthArray, heightArray) + return widthArray[0] to heightArray[0] + } + + override fun getHeight(): Int { + return getSize().second + } + + override fun updateFrameBuffer(resolutionManager: ResolutionManager) { + buffer.updateFrameBuffer(resolutionManager) + } + + override fun cleanup() { + glfwDestroyWindow(windowPointer) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWInputReceiver.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWInputReceiver.kt new file mode 100644 index 00000000..3f90d04c --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWInputReceiver.kt @@ -0,0 +1,101 @@ +package gg.essential.elementa.components.inspector.display.glfw + +import gg.essential.elementa.components.Window +import gg.essential.elementa.impl.Platform +import gg.essential.elementa.manager.KeyboardManager +import gg.essential.elementa.manager.MousePositionManager +import gg.essential.elementa.manager.ResolutionManager +import gg.essential.universal.UKeyboard +import org.lwjgl.glfw.GLFW + + +internal class GLFWInputReceiver( + private val windowPointer: Long, + private val resolutionManager: ResolutionManager, + private val window: Window, +) : MousePositionManager, KeyboardManager { + + + override var rawX: Double = -1.0 + + override var rawY: Double = -1.0 + + override val scaledX: Double + get() = rawX / resolutionManager.scaleFactor + + override val scaledY: Double + get() = rawY / resolutionManager.scaleFactor + + private var allowRepeatEvents = true + + private fun runOnMinecraftThread(runnable: () -> Unit) { + Platform.platform.runOnMinecraftThread(runnable) + } + + + init { + GLFW.glfwSetCursorPosCallback(windowPointer) { _: Long, x: Double, y: Double -> + runOnMinecraftThread { + this.rawX = x + this.rawY = y + } + } + GLFW.glfwSetMouseButtonCallback(windowPointer) { _: Long, button: Int, action: Int, _: Int -> + runOnMinecraftThread { + if (action == GLFW.GLFW_PRESS) { + window.mouseClick( + rawX / resolutionManager.scaleFactor, + rawY / resolutionManager.scaleFactor, + button + ) + } else if (action == GLFW.GLFW_RELEASE) { + window.mouseRelease() + } + } + } + GLFW.glfwSetScrollCallback(windowPointer) { _: Long, _: Double, vertical: Double -> + runOnMinecraftThread { + window.mouseScroll(vertical) + } + } + GLFW.glfwSetKeyCallback(windowPointer) { _: Long, key: Int, _: Int, action: Int, _: Int -> + runOnMinecraftThread { + if (key == 0) { + return@runOnMinecraftThread + } + + if (action == GLFW.GLFW_PRESS || (action == GLFW.GLFW_REPEAT && allowRepeatEvents)) { + window.keyType(0.toChar(), key) + } + } + } + GLFW.glfwSetCharModsCallback(windowPointer) { _: Long, keyInt: Int, _: Int -> + runOnMinecraftThread { + if (Character.charCount(keyInt) == 1) { + window.keyType(keyInt.toChar(), 0) + } else { + val chars = Character.toChars(keyInt) + for (char in chars) { + window.keyType(char, 0) + } + } + } + } + } + + override fun isKeyDown(key: Int): Boolean { + val state = if (key < 20) GLFW.glfwGetMouseButton(windowPointer, key) else GLFW.glfwGetKey(windowPointer, key) + return state == GLFW.GLFW_PRESS + } + + override fun allowRepeatEvents(enabled: Boolean) { + allowRepeatEvents = enabled + } + + override fun getModifiers(): UKeyboard.Modifiers = UKeyboard.Modifiers( + UKeyboard.isCtrlKeyDown(), + UKeyboard.isShiftKeyDown(), + UKeyboard.isAltKeyDown(), + ) + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWRenderer.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWRenderer.kt new file mode 100644 index 00000000..cc53ae74 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/display/glfw/GLFWRenderer.kt @@ -0,0 +1,48 @@ +package gg.essential.elementa.components.inspector.display.glfw + +import gg.essential.elementa.debug.FrameBufferedWindow +import org.lwjgl.glfw.GLFW +import org.lwjgl.opengl.GL +import org.lwjgl.opengl.GL11 +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +internal class GLFWRenderer( + private val frameBuffer: FrameBufferedWindow, + private val window: Long, + private val display: GLFWDisplay, +) { + private val workerPool = Executors.newSingleThreadScheduledExecutor() + private var contextCurrent = false + + init { + schedule(1000 / 30, TimeUnit.MILLISECONDS) { + if (!display.visible) { + return@schedule + } + if (!contextCurrent) { + GLFW.glfwMakeContextCurrent(window) + GL.createCapabilities() + } + val width = display.getWidth() + val height = display.getHeight() + GL11.glViewport(0, 0, width, height) + GL11.glClear(GL11.GL_COLOR_BUFFER_BIT) + GL11.glMatrixMode(GL11.GL_PROJECTION) + GL11.glLoadIdentity() + GL11.glOrtho(0.0, width.toDouble(), height.toDouble(), 0.0, -1.0, 1.0) + GL11.glMatrixMode(GL11.GL_MODELVIEW) + GL11.glPushMatrix() + GL11.glColor3f(1f, 1f, 1f) + + frameBuffer.renderFrameBufferTexture() + + GL11.glPopMatrix() + GLFW.glfwSwapBuffers(window) + } + } + + private fun schedule(period: Long, unit: TimeUnit, runnable: () -> Unit) { + workerPool.scheduleAtFixedRate(runnable, 0, period, unit) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/state/CompactSelector.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/state/CompactSelector.kt new file mode 100644 index 00000000..1541fc7f --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/state/CompactSelector.kt @@ -0,0 +1,66 @@ +package gg.essential.elementa.components.inspector.state + +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.dsl.childOf +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.provideDelegate +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.bindParent +import gg.essential.elementa.utils.hoveredState +import gg.essential.elementa.utils.onLeftClick +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +class CompactSelector( + private val options: List, + private val state: State, + private val mapper: (T) -> String, +) : UIContainer() { + + private val selectorOpen = BasicState(false) + + private val selectedOption by UIText().bindText(state.map(mapper)).onLeftClick { + selectorOpen.set { !it } + } childOf this + + private val optionsContainer by UIBlock(Color(40, 40, 40)).constrain { + y = SiblingConstraint() + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + }.bindParent(this, selectorOpen) + + init { + constrain { + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } + options.forEach { option -> + val optionText by UIText(mapper(option)).constrain { + y = SiblingConstraint(2f) + } childOf optionsContainer + + optionText.setColor( + optionText.hoveredState().map { + if (it) { + Color.WHITE + } else { + Color(0xBBBBBB) + } + }.toConstraint() + ) + + optionText.onLeftClick { + state.set(option) + selectorOpen.set(false) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/state/StateTextInput.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/state/StateTextInput.kt new file mode 100644 index 00000000..557e754f --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/state/StateTextInput.kt @@ -0,0 +1,107 @@ +package gg.essential.elementa.components.inspector.state + +import gg.essential.elementa.components.input.v2.UITextInput +import gg.essential.elementa.constraints.animation.AnimatingConstraints +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.onLeftClick +import gg.essential.elementa.utils.onSetValueAndNow +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +/** + * Simple text input that sets [state] on enter and focus lost. The text is converted to `T` using [parse]. + * If the input is not valid (that is, [parse] throws a [ParseException]), an error animation is shown. + */ +class StateTextInput( + private val state: State, + mutable: Boolean, + textPadding: Float = 2f, + private val formatToText: (T) -> String, + private val parse: (String) -> T, +) : UITextInput() { + + init { + if (mutable) { + onLeftClick { + grabWindowFocus() + } + } + constrain { + width = basicWidthConstraint { + getText().width() + textPadding + if (active) 1f else 0f + } + color = Color(0xAAAAAA).toConstraint() + } + + onFocusLost { + if (!updateState()) { + cloneStateToInput() + } + } + state.onSetValueAndNow { + cloneStateToInput() + } + } + + /** + * Sets the value of the input to the current value of the state + */ + private fun cloneStateToInput() { + setText(formatToText(state.get())) + } + + override fun onEnterPressed() { + if (updateState()) { + cloneStateToInput() + } + } + + /** + * Tries to update the state based on the current value of the text input. + * Returns true if the state was updated. + */ + private fun updateState(): Boolean { + val mappedValue = try { + parse(getText()) + } catch (e: ParseException) { + animateError() + return false + } + state.set(mappedValue) + return true + } + + /** + * Plays an animation indicating that the value is invalid. + */ + private fun animateError() { + // Already animating + if (constraints is AnimatingConstraints) { + return + } + val oldSelectionForegroundColor = selectionForegroundColor + val oldInactiveSelectionForegroundColor = inactiveSelectionForegroundColor + val oldColor = getColor() + val oldX = constraints.x + selectionForegroundColor = Color.RED + inactiveSelectionForegroundColor = Color.RED + setColor(Color.RED) + animate { + setXAnimation(Animations.IN_BOUNCE, .25f, oldX + 3.pixels) + onComplete { + setX(oldX) + setColor(oldColor) + selectionForegroundColor = oldSelectionForegroundColor + inactiveSelectionForegroundColor = oldInactiveSelectionForegroundColor + } + } + } + + /** + * Thrown to show that the current value of the text input is not valid. + */ + @ApiStatus.Internal + class ParseException : Exception() +} diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ConstraintsTab.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ConstraintsTab.kt new file mode 100644 index 00000000..df01a24c --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ConstraintsTab.kt @@ -0,0 +1,164 @@ +package gg.essential.elementa.components.inspector.tabs + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.UIConstraints +import gg.essential.elementa.components.TreeListComponent +import gg.essential.elementa.components.TreeNode +import gg.essential.elementa.components.inspector.InfoBlockNode +import gg.essential.elementa.constraints.* +import gg.essential.elementa.constraints.animation.* +import gg.essential.elementa.dsl.childOf +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.provideDelegate +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +class ConstraintsTab : InspectorTab("Constraints") { + + private val constraintsContent by TreeListComponent().constrain { + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf this + + private val currentRoots = mutableMapOf() + + + private fun setNewConstraints(constraints: UIConstraints) { + setConstraintNodes(constraints) + + constraints.addObserver { _, arg -> + if (arg !is ConstraintType) + return@addObserver + val constraint = when (arg) { + ConstraintType.X -> constraints.x + ConstraintType.Y -> constraints.y + ConstraintType.WIDTH -> constraints.width + ConstraintType.HEIGHT -> constraints.height + ConstraintType.RADIUS -> constraints.radius + ConstraintType.COLOR -> constraints.color + ConstraintType.TEXT_SCALE -> constraints.textScale + ConstraintType.FONT_PROVIDER -> constraints.fontProvider + } + + when (arg) { + ConstraintType.COLOR -> { + currentRoots[arg] = + if (constraint !is ConstantColorConstraint || constraint.color != Color.WHITE) getNodeFromConstraint( + constraint, + arg.prettyName + ) else null + } + ConstraintType.TEXT_SCALE -> { + currentRoots[ConstraintType.TEXT_SCALE] = + if (constraint !is PixelConstraint || constraint.value != 1f) getNodeFromConstraint( + constraint, + arg.prettyName + ) else null + } + else -> { + currentRoots[arg] = + if (constraint !is PixelConstraint || constraint.value != 0f) getNodeFromConstraint( + constraint, + arg.prettyName + ) else null + } + } + + constraintsContent.setRoots(currentRoots.values.filterNotNull()) + } + } + + private fun setConstraintNodes(constraints: UIConstraints) { + currentRoots.clear() + + listOf( + constraints.x to ConstraintType.X, + constraints.y to ConstraintType.Y, + constraints.width to ConstraintType.WIDTH, + constraints.height to ConstraintType.HEIGHT, + constraints.radius to ConstraintType.RADIUS + ).forEach { (constraint, type) -> + currentRoots[type] = + if (constraint !is PixelConstraint || constraint.value != 0f) getNodeFromConstraint( + constraint, + type.prettyName + ) else null + } + + constraints.textScale.also { + currentRoots[ConstraintType.TEXT_SCALE] = + if (it !is PixelConstraint || it.value != 1f) getNodeFromConstraint(it, "TextScale") else null + } + + constraints.color.also { + currentRoots[ConstraintType.COLOR] = + if (it !is ConstantColorConstraint || it.color != Color.WHITE) getNodeFromConstraint( + it, + "Color" + ) else null + } + + constraintsContent.setRoots(currentRoots.values.filterNotNull()) + } + + private fun getNodeFromConstraint(constraint: SuperConstraint<*>, name: String? = null): TreeNode { + val baseInfoNode = InfoBlockNode(constraint, name) + + return when (constraint) { + is AdditiveConstraint -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.constraint1)) + add(getNodeFromConstraint(constraint.constraint2)) + } + is CoerceAtMostConstraint -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.constraint)) + add(getNodeFromConstraint(constraint.maxConstraint)) + } + is CoerceAtLeastConstraint -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.constraint)) + add(getNodeFromConstraint(constraint.minConstraint)) + } + is SubtractiveConstraint -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.constraint1)) + add(getNodeFromConstraint(constraint.constraint2)) + } + is XAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + is YAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + is WidthAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + is HeightAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + is RadiusAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + is ColorAnimationComponent -> baseInfoNode.withChildren { + add(getNodeFromConstraint(constraint.oldConstraint, name = "From")) + add(getNodeFromConstraint(constraint.newConstraint, name = "To")) + } + else -> baseInfoNode + } + } + + override fun updateWithComponent(component: UIComponent) { + setNewConstraints(component.constraints) + component.addObserver { _, arg -> + if (arg is UIConstraints) { + setNewConstraints(arg) + } + } + } + + override fun updateValues() { + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/InspectorTab.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/InspectorTab.kt new file mode 100644 index 00000000..3260ffd6 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/InspectorTab.kt @@ -0,0 +1,35 @@ +package gg.essential.elementa.components.inspector.tabs + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.dsl.constrain +import org.jetbrains.annotations.ApiStatus + + +@ApiStatus.Internal +abstract class InspectorTab(val name: String) : UIContainer() { + + protected var targetComponent: UIComponent? = null + + init { + constrain { + x = CenterConstraint() + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } + } + + fun newComponent(component: UIComponent?) { + targetComponent = component + if (component != null) { + updateWithComponent(component) + } + } + + abstract fun updateWithComponent(component: UIComponent) + + abstract fun updateValues() +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/StatesTab.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/StatesTab.kt new file mode 100644 index 00000000..93e36e70 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/StatesTab.kt @@ -0,0 +1,71 @@ +package gg.essential.elementa.components.inspector.tabs + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.components.inspector.state.StateTextInput +import gg.essential.elementa.constraints.* +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.debug.StateRegistryComponentFactory +import gg.essential.elementa.dsl.* +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +class StatesTab : InspectorTab("States") { + + override fun updateWithComponent(component: UIComponent) { + clearChildren() + if (component is StateRegistry) { + val managedStates = component.managedStates + managedStates.forEach { + createStateViewer(it, this) + } + } + } + + override fun updateValues() { + } + + @ApiStatus.Internal + companion object { + + fun createStateViewer(managedState: ManagedState, parent: UIComponent) { + val container = createContainer(managedState.name, parent) + val component = StateRegistryComponentFactory.createInspectorComponent(managedState) childOf container + + component.constrain { + if (component is StateTextInput<*>) { + y = (-1).pixels + } + x = SiblingConstraint(3f) + } + + if (managedState is ManagedState.OfColor) { + UIBlock(managedState.state).constrain { + width = 7.pixels + height = AspectConstraint() + x = SiblingConstraint(3f) + } childOf container + } + } + + private fun createContainer(name: String, parent: UIComponent): UIContainer { + val container by UIContainer().constrain { + y = SiblingConstraint() + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + }.addChild( + UIText(name).constrain { + y = SiblingConstraint() + color = Color(0xAAAAAA).toConstraint() + } + ) childOf parent + + return container as UIContainer + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ValuesTab.kt b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ValuesTab.kt new file mode 100644 index 00000000..bb49e3cf --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/components/inspector/tabs/ValuesTab.kt @@ -0,0 +1,172 @@ +package gg.essential.elementa.components.inspector.tabs + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIContainer +import gg.essential.elementa.components.UIText +import gg.essential.elementa.constraints.* +import gg.essential.elementa.constraints.debug.CycleSafeConstraintDebugger +import gg.essential.elementa.constraints.debug.withDebugger +import gg.essential.elementa.dsl.* +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.toConstraint +import gg.essential.elementa.utils.bindParent +import gg.essential.elementa.utils.not +import gg.essential.elementa.utils.onLeftClick +import org.jetbrains.annotations.ApiStatus +import java.awt.Color + +@ApiStatus.Internal +class ValuesTab : InspectorTab("Values") { + + private val grid by UIContainer().constrain { + x = 5.pixels + width = ChildBasedMaxSizeConstraint() + height = ChildBasedSizeConstraint() + } childOf this + + private val firstRow by UIContainer().constrain { + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + } childOf grid + + private val secondRow by UIContainer().constrain { + y = SiblingConstraint() + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + } childOf grid + + private val thirdRow = UIContainer().constrain { + x = 5.pixels + y = SiblingConstraint() + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + } childOf this + + private val valueList = mutableListOf() + + private val leftValue by createValue("Left", firstRow) { getLeft() } + private val topValue by createValue("Top", firstRow) { getTop() } + private val widthValue by createValue("Width", firstRow) { getWidth() } + private val rightValue by createValue("Right", secondRow) { getRight() } + private val bottomValue by createValue("Bottom", secondRow) { getBottom() } + private val heightValue by createValue("Height", secondRow) { getHeight() } + + private val radiusValueText by createValue("Radius", thirdRow, { getRadius() }, { it == 0f }) + private val textScaleValue by createValue("TextScale", thirdRow, { getTextScale() }, { it == 1f }) + + + private var useColorHex = true + private val componentColor = BasicState(Color.WHITE) + private val colorValueText = Value("Color") { + val color = getColor() + componentColor.set(color) + if (color.rgb == Color.WHITE.rgb) { + return@Value null + } else if (useColorHex) { + if (color.alpha == 255) { + "0x${Integer.toHexString(color.rgb and (0xFFFFFF)).uppercase()}" + } else { + "0x${Integer.toHexString(color.rgb).uppercase()}" + } + + } else { + if (color.alpha == 255) { + "Color(%d, %d, %d)".format(color.red, color.green, color.blue) + } else { + "Color(%d, %d, %d, %d)".format(color.red, color.green, color.blue, color.alpha) + } + } + }.apply { + onLeftClick { + useColorHex = !useColorHex + } + bindParent(thirdRow, !hidden) + registerCustomComponent(UIBlock().setColor(componentColor.toConstraint()).constrain { + width = 7.pixels + height = AspectConstraint() + }) + } + + private fun createValue(name: String, parent: UIComponent, getter: UIComponent.() -> Float): Value { + return Value(name, getter).apply { + bindParent(parent, !hidden) + } + } + + private fun createValue( + name: String, + parent: UIComponent, + getter: UIComponent.() -> Float, + filter: (Float) -> Boolean = { true } + ): Value { + return Value(name, getter, filter).apply { + bindParent(parent, hidden) + } + } + + private inner class Value(name: String, private val extractor: UIComponent.() -> String?) : UIContainer() { + + constructor( + name: String, + floatExtractor: (UIComponent) -> Float?, + filter: (Float) -> Boolean = { true }, + ) : this( + name, + extractor = { + val float = floatExtractor(this) + if (float != null && filter(float)) { + float.toString() + } else { + null + } + }) + + val hidden = BasicState(false) + + private val nameText by UIText("$name: ").constrain { + width = TextAspectConstraint() + } childOf this + + private val valueText by UIText("").constrain { + x = SiblingConstraint() + width = TextAspectConstraint() + } childOf this + + + init { + constrain { + x = ColumnPositionConstraint(10f) + width = ChildBasedSizeConstraint() + height = ChildBasedMaxSizeConstraint() + 3.pixels() + } + valueList.add(this) + } + + fun update(component: UIComponent) { + val value = extractor(component) + hidden.set(value == null) + if (value != null) { + valueText.setText(value) + } + } + + fun registerCustomComponent(uiComponent: UIComponent) { + uiComponent.constrain { + x = SiblingConstraint() + } childOf this + } + } + + override fun updateWithComponent(component: UIComponent) { + } + + override fun updateValues() { + val cachedComponent = targetComponent ?: return + withDebugger(CycleSafeConstraintDebugger()) { + valueList.forEach { + it.update(cachedComponent) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/constraints/AspectConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/AspectConstraint.kt index 4ae49b47..b2120d07 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/AspectConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/AspectConstraint.kt @@ -2,6 +2,12 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.getValue +import org.jetbrains.annotations.ApiStatus /** * For size: @@ -10,29 +16,34 @@ import gg.essential.elementa.constraints.resolution.ConstraintVisitor * For position: * Sets the x/y position to be [value] multiple of its own y/x position respectively. */ -class AspectConstraint @JvmOverloads constructor(val value: Float = 1f) : PositionConstraint, SizeConstraint { +class AspectConstraint(private val valueState: State) : PositionConstraint, SizeConstraint, StateRegistry { + + @JvmOverloads constructor(value: Float = 1f) : this(BasicState(value)) + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null + val value by valueState + override fun getXPositionImpl(component: UIComponent): Float { - return (constrainTo ?: component).getTop() * value + return (constrainTo ?: component).getTop() * valueState.get() } override fun getYPositionImpl(component: UIComponent): Float { - return (constrainTo ?: component).getLeft()* value + return (constrainTo ?: component).getLeft()* valueState.get() } override fun getWidthImpl(component: UIComponent): Float { - return (constrainTo ?: component).getHeight() * value + return (constrainTo ?: component).getHeight() * valueState.get() } override fun getHeightImpl(component: UIComponent): Float { - return (constrainTo ?: component).getWidth() * value + return (constrainTo ?: component).getWidth() * valueState.get() } override fun getRadiusImpl(component: UIComponent): Float { - return (constrainTo ?: component).getRadius() * value + return (constrainTo ?: component).getRadius() * valueState.get() } override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { @@ -45,4 +56,10 @@ class AspectConstraint @JvmOverloads constructor(val value: Float = 1f) : Positi else -> throw IllegalArgumentException(type.prettyName) } } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(valueState, "value", true) + ) } \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/constraints/CenterConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/CenterConstraint.kt index 9235dc07..bea7f37e 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/CenterConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/CenterConstraint.kt @@ -16,9 +16,9 @@ class CenterConstraint : PositionConstraint { val parent = constrainTo ?: component.parent return if (component.isPositionCenter()) { - parent.getLeft() + (parent.getWidth() / 2).roundToRealPixels() + parent.getLeft() + (parent.getWidth() / 2).roundToRealPixels(component) } else { - parent.getLeft() + (parent.getWidth() / 2 - component.getWidth() / 2).roundToRealPixels() + parent.getLeft() + (parent.getWidth() / 2 - component.getWidth() / 2).roundToRealPixels(component) } } @@ -26,9 +26,9 @@ class CenterConstraint : PositionConstraint { val parent = constrainTo ?: component.parent return if (component.isPositionCenter()) { - parent.getTop() + (parent.getHeight() / 2).roundToRealPixels() + parent.getTop() + (parent.getHeight() / 2).roundToRealPixels(component) } else { - parent.getTop() + (parent.getHeight() / 2 - component.getHeight() / 2).roundToRealPixels() + parent.getTop() + (parent.getHeight() / 2 - component.getHeight() / 2).roundToRealPixels(component) } } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ChildBasedConstraints.kt b/src/main/kotlin/gg/essential/elementa/constraints/ChildBasedConstraints.kt index 25e34329..c2e078f3 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/ChildBasedConstraints.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/ChildBasedConstraints.kt @@ -2,11 +2,24 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.getValue +import org.jetbrains.annotations.ApiStatus /** * Sets this component's width or height to be the sum of its children's width or height */ -class ChildBasedSizeConstraint(val padding: Float = 0f) : SizeConstraint { +class ChildBasedSizeConstraint( + private val paddingState: State, +) : SizeConstraint, StateRegistry { + + @JvmOverloads constructor(padding: Float = 0f) : this(BasicState(padding)) + + val padding by paddingState + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -15,14 +28,14 @@ class ChildBasedSizeConstraint(val padding: Float = 0f) : SizeConstraint { val holder = (constrainTo ?: component) return holder.children.sumOf { it.getWidth() + ((it.constraints.x as? PaddingConstraint)?.getHorizontalPadding(it) ?: 0f).toDouble() - }.toFloat() + (holder.children.size - 1) * padding + }.toFloat() + (holder.children.size - 1) * paddingState.get() } override fun getHeightImpl(component: UIComponent): Float { val holder = (constrainTo ?: component) return holder.children.sumOf { it.getHeight() + ((it.constraints.y as? PaddingConstraint)?.getVerticalPadding(it) ?: 0f).toDouble() - }.toFloat() + (holder.children.size - 1) * padding + }.toFloat() + (holder.children.size - 1) * paddingState.get() } override fun getRadiusImpl(component: UIComponent): Float { @@ -37,6 +50,12 @@ class ChildBasedSizeConstraint(val padding: Float = 0f) : SizeConstraint { else -> throw IllegalArgumentException(type.prettyName) } } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(paddingState, "padding", true) + ) } class ChildBasedMaxSizeConstraint : SizeConstraint { diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt b/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt index d59fbcf6..1900b393 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/ColorConstraints.kt @@ -2,10 +2,14 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State import gg.essential.elementa.dsl.* import gg.essential.elementa.state.MappedState +import gg.essential.elementa.utils.getValue +import org.jetbrains.annotations.ApiStatus import java.awt.Color import kotlin.math.sin import kotlin.random.Random @@ -13,8 +17,12 @@ import kotlin.random.Random /** * Sets the color to be a constant, determined color. */ -class ConstantColorConstraint(color: State) : ColorConstraint { - @JvmOverloads constructor(color: Color = Color.WHITE) : this(BasicState(color)) +class ConstantColorConstraint( + color: State, +) : ColorConstraint, StateRegistry { + @JvmOverloads + constructor(color: Color = Color.WHITE) : this(BasicState(color)) + override var cachedValue: Color = Color.WHITE override var recalculate = true override var constrainTo: UIComponent? = null @@ -23,7 +31,9 @@ class ConstantColorConstraint(color: State) : ColorConstraint { var color: Color get() = colorState.get() - set(value) { colorState.set(value) } + set(value) { + colorState.set(value) + } fun bindColor(newState: State) = apply { colorState.rebind(newState) @@ -39,15 +49,25 @@ class ConstantColorConstraint(color: State) : ColorConstraint { // Color constraints will only ever have parent dependencies, so there is no possibility // of an invalid constraint here - override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { } + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) {} + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfColor(colorState, "color", true) + ) } /** * Sets the color to be constant but with an alpha based off of its parent. */ -class AlphaAspectColorConstraint(color: State, alphaValue: State) : ColorConstraint { +class AlphaAspectColorConstraint( + color: State, + alphaValue: State, +) : ColorConstraint, StateRegistry { constructor(color: Color = Color.WHITE, alphaValue: Float = 1f) : this(BasicState(color), BasicState(alphaValue)) constructor() : this(Color.WHITE, 1f) + override var cachedValue: Color = Color.WHITE override var recalculate = true override var constrainTo: UIComponent? = null @@ -57,10 +77,14 @@ class AlphaAspectColorConstraint(color: State, alphaValue: State) var color: Color get() = colorState.get() - set(value) { colorState.set(value) } + set(value) { + colorState.set(value) + } var alpha: Float get() = alphaState.get() - set(value) { alphaState.set(value) } + set(value) { + alphaState.set(value) + } fun bindColor(newState: State) = apply { colorState.rebind(newState) @@ -78,14 +102,31 @@ class AlphaAspectColorConstraint(color: State, alphaValue: State) // Color constraints will only ever have parent dependencies, so there is no possibility // of an invalid constraint here - override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { } + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) {} + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfColor(colorState, "color", true), + ManagedState.OfFloat(alphaState, "alpha", true) + ) } /** * Changes this component's color every frame, using a sin wave to create * a chroma effect. */ -class RainbowColorConstraint(val alpha: Int = 255, val speed: Float = 50f) : ColorConstraint { +class RainbowColorConstraint( + alpha: State, + speed: State, +) : ColorConstraint, StateRegistry { + + private val alphaState = alpha.map { it } + private val speedState = speed.map { it } + + @JvmOverloads + constructor(alpha: Int = 255, speed: Float = 50f) : this(BasicState(alpha), BasicState(speed)) + override var cachedValue = Color.WHITE override var recalculate = true override var constrainTo: UIComponent? = null @@ -93,6 +134,9 @@ class RainbowColorConstraint(val alpha: Int = 255, val speed: Float = 50f) : Col private var currentColor: Color = Color.WHITE private var currentStep = Random.nextInt(500) + val speed by speedState + val alpha by alphaState + override fun getColorImpl(component: UIComponent): Color { return currentColor } @@ -118,5 +162,12 @@ class RainbowColorConstraint(val alpha: Int = 255, val speed: Float = 50f) : Col // Color constraints will only ever have parent dependencies, so there is no possibility // of an invalid constraint here - override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { } + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) {} + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(speedState, "speed", true), + ManagedState.OfInt(alphaState, "alpha", true) + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ColumnPositionConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/ColumnPositionConstraint.kt new file mode 100644 index 00000000..ae773646 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/constraints/ColumnPositionConstraint.kt @@ -0,0 +1,74 @@ +package gg.essential.elementa.constraints + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import org.jetbrains.annotations.ApiStatus + +/** + * Column Position Constraint is a constraint that allows the creation of a vertically + * align column of components. The X position of the column is determined by the + * finding the largest right edge of all siblings one index less than the component + * and adding the padding. + * + * Example: + * UIContainer ("Box") + * - UIContainer ("Row 1") + * - UIComponent ("Component 1") with width=15 + * - UIComponent ("Component 2") with x=ColumnPositionConstraint(10f) + * - UIContainer ("Row 2") + * - UIComponent ("Component 3") with width=25 + * - UIComponent ("Component 4") with x=ColumnPositionConstraint(10f) + * The X value component 2 and 4 will be 25 + 10 = 35 pixels to the right of Box + * + */ +class ColumnPositionConstraint( + padding: State, +) : XConstraint, PaddingConstraint, StateRegistry { + + private val paddingState = padding.map { it } + + constructor(padding: Float) : this(BasicState(padding)) + + override var cachedValue = 0f + override var recalculate = true + override var constrainTo: UIComponent? = null + set(_) = throw IllegalArgumentException("Cannot constrain ColumnPositionConstraint to a component!") + + override fun getXPositionImpl(component: UIComponent): Float { + val currentRow = component.parent + val previousColumnIndex = currentRow.children.indexOf(component) - 1 + if (previousColumnIndex < 0) { + return currentRow.getLeft() + } + return currentRow.parent.children.maxOf { + it.children.getOrNull(previousColumnIndex)?.getRight() ?: 0f + } + paddingState.get() + } + + override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { + } + + override fun getHorizontalPadding(component: UIComponent): Float { + val currentRow = component.parent + val previousColumnIndex = currentRow.children.indexOf(component) - 1 + return if (previousColumnIndex < 0) { + return 0f + } else { + paddingState.get() + } + } + + override fun getVerticalPadding(component: UIComponent): Float { + return 0f + } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(paddingState, "padding", true) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt index db402196..3a02c2b1 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/Constraint.kt @@ -79,7 +79,7 @@ interface XConstraint : SuperConstraint { } if (recalculate) { - cachedValue = getXPositionImpl(component).roundToRealPixels() + cachedValue = getXPositionImpl(component).roundToRealPixels(component) recalculate = false } @@ -97,7 +97,7 @@ interface YConstraint : SuperConstraint { } if (recalculate) { - cachedValue = getYPositionImpl(component).roundToRealPixels() + cachedValue = getYPositionImpl(component).roundToRealPixels(component) recalculate = false } @@ -135,7 +135,7 @@ interface WidthConstraint : SuperConstraint { } if (recalculate) { - cachedValue = getWidthImpl(component).roundToRealPixels() + cachedValue = getWidthImpl(component).roundToRealPixels(component) recalculate = false } @@ -153,7 +153,7 @@ interface HeightConstraint : SuperConstraint { } if (recalculate) { - cachedValue = getHeightImpl(component).roundToRealPixels() + cachedValue = getHeightImpl(component).roundToRealPixels(component) recalculate = false } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/MousePositionConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/MousePositionConstraint.kt index 4133b25f..a0d09432 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/MousePositionConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/MousePositionConstraint.kt @@ -2,6 +2,7 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.utils.mousePositionManager class MousePositionConstraint : PositionConstraint { override var cachedValue = 0f @@ -9,11 +10,11 @@ class MousePositionConstraint : PositionConstraint { override var constrainTo: UIComponent? = null override fun getXPositionImpl(component: UIComponent): Float { - return UIComponent.getMouseX() + return component.mousePositionManager.scaledX.toFloat() } override fun getYPositionImpl(component: UIComponent): Float { - return UIComponent.getMouseY() + return component.mousePositionManager.scaledY.toFloat() } override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/PixelConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/PixelConstraint.kt index d9ed974f..de4546cd 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/PixelConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/PixelConstraint.kt @@ -2,9 +2,12 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.MappedState import gg.essential.elementa.state.State +import org.jetbrains.annotations.ApiStatus /** * Sets this component's X/Y position or width/height to be a constant @@ -13,8 +16,8 @@ import gg.essential.elementa.state.State class PixelConstraint @JvmOverloads constructor( value: State, alignOpposite: State = BasicState(false), - alignOutside: State = BasicState(false) -) : MasterConstraint { + alignOutside: State = BasicState(false), +) : MasterConstraint, StateRegistry { @JvmOverloads constructor( value: Float, alignOpposite: Boolean = false, @@ -133,4 +136,12 @@ class PixelConstraint @JvmOverloads constructor( else -> throw IllegalArgumentException(type.prettyName) } } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(valueState, "value", true), + ManagedState.OfBoolean(alignOppositeState, "alignOpposite", true), + ManagedState.OfBoolean(alignOutsideState, "alignOutside", true), + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/RelativeConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/RelativeConstraint.kt index 7ca91103..055b48c2 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/RelativeConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/RelativeConstraint.kt @@ -2,16 +2,24 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.MappedState import gg.essential.elementa.state.State +import org.jetbrains.annotations.ApiStatus /** * Sets this component's X/Y position or width/height to be some * multiple of its parents. */ -class RelativeConstraint constructor(value: State) : PositionConstraint, SizeConstraint { - @JvmOverloads constructor(value: Float = 1f) : this(BasicState(value)) +class RelativeConstraint constructor( + value: State, +) : PositionConstraint, SizeConstraint, StateRegistry { + + @JvmOverloads + constructor(value: Float = 1f) : this(BasicState(value)) + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -20,7 +28,9 @@ class RelativeConstraint constructor(value: State) : PositionConstraint, var value: Float get() = valueState.get() - set(value) { valueState.set(value) } + set(value) { + valueState.set(value) + } fun bindValue(newState: State) = apply { valueState.rebind(newState) @@ -62,4 +72,10 @@ class RelativeConstraint constructor(value: State) : PositionConstraint, else -> throw IllegalArgumentException(type.prettyName) } } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(valueState, "value", true), + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/RelativeWindowConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/RelativeWindowConstraint.kt index b1bcb7dd..0ae60634 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/RelativeWindowConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/RelativeWindowConstraint.kt @@ -3,12 +3,26 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.getValue +import org.jetbrains.annotations.ApiStatus /** * Sets this component's X/Y position or width/height to be some percentage * of the Window */ -class RelativeWindowConstraint @JvmOverloads constructor(val value: Float = 1f) : PositionConstraint, SizeConstraint { +class RelativeWindowConstraint( + value: State, +) : PositionConstraint, SizeConstraint, StateRegistry { + @JvmOverloads constructor(value: Float = 1f): this(BasicState(value)) + + private val valueState: State = value.map { it } + + val value by valueState + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -50,4 +64,10 @@ class RelativeWindowConstraint @JvmOverloads constructor(val value: Float = 1f) if (constrainTo == null) constrainTo = Window.of(component) } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(valueState, "value", true), + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/RoundingConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/RoundingConstraint.kt index 484bcf72..c053a4ea 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/RoundingConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/RoundingConstraint.kt @@ -2,6 +2,13 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.getValue +import gg.essential.elementa.utils.setValue +import org.jetbrains.annotations.ApiStatus import kotlin.math.ceil import kotlin.math.floor import kotlin.math.round @@ -10,10 +17,20 @@ import kotlin.math.round * Rounds a constraint to an Int value using one of three modes: floor, * ceil, and rounding. Rounding is the default. */ -class RoundingConstraint @JvmOverloads constructor( +class RoundingConstraint constructor( val constraint: SuperConstraint, - var roundingMode: Mode = Mode.Round -) : MasterConstraint { + roundingMode: State, +) : MasterConstraint, StateRegistry { + + @JvmOverloads constructor( + constraint: SuperConstraint, + roundingMode: Mode = Mode.Round, + ): this(constraint, BasicState(roundingMode)) + + private val roundingModeState: State = roundingMode.map { it } + + var roundingMode by roundingModeState + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -53,4 +70,10 @@ class RoundingConstraint @JvmOverloads constructor( Ceil, Round, } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfEnum(roundingModeState, "mode", true), + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt index c6f49718..0d1f613e 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/ScaleConstraint.kt @@ -2,11 +2,17 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.MappedState import gg.essential.elementa.state.State +import org.jetbrains.annotations.ApiStatus -class ScaleConstraint(val constraint: SuperConstraint, value: State) : MasterConstraint { +class ScaleConstraint( + val constraint: SuperConstraint, + value: State, +) : MasterConstraint, StateRegistry { constructor(constraint: SuperConstraint, value: Float) : this(constraint, BasicState(value)) override var cachedValue = 0f override var recalculate = true @@ -53,4 +59,10 @@ class ScaleConstraint(val constraint: SuperConstraint, value: State, +) : SizeConstraint, StateRegistry { + + constructor(scale: Float): this(BasicState(scale)) + + private val scaleState: State = scale.map { it } + + var scale: Float by scaleState + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -38,4 +54,10 @@ class ScaledTextConstraint(var scale: Float) : SizeConstraint { } override fun visitImpl(visitor: ConstraintVisitor, type: ConstraintType) { } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(scaleState, "scale", true), + ) } diff --git a/src/main/kotlin/gg/essential/elementa/constraints/SiblingConstraint.kt b/src/main/kotlin/gg/essential/elementa/constraints/SiblingConstraint.kt index f962125e..e1eab2ec 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/SiblingConstraint.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/SiblingConstraint.kt @@ -2,6 +2,12 @@ package gg.essential.elementa.constraints import gg.essential.elementa.UIComponent import gg.essential.elementa.constraints.resolution.ConstraintVisitor +import gg.essential.elementa.debug.ManagedState +import gg.essential.elementa.debug.StateRegistry +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.getValue +import org.jetbrains.annotations.ApiStatus /** * Positions this component to be directly after its previous sibling. @@ -9,10 +15,22 @@ import gg.essential.elementa.constraints.resolution.ConstraintVisitor * Intended for use in either the x or y direction but not both at the same time. * If you would like for components to try and fit inline, use [CramSiblingConstraint] */ -open class SiblingConstraint @JvmOverloads constructor( - val padding: Float = 0f, - val alignOpposite: Boolean = false -) : PositionConstraint, PaddingConstraint { +open class SiblingConstraint constructor( + padding: State, + alignOpposite: State, +) : PositionConstraint, PaddingConstraint, StateRegistry { + + @JvmOverloads constructor( + padding: Float = 0f, + alignOpposite: Boolean = false + ): this(BasicState(padding), BasicState(alignOpposite)) + + private val paddingState: State = padding.map { it } + private val alignOppositeState: State = alignOpposite.map { it } + + val padding by paddingState + val alignOpposite by alignOppositeState + override var cachedValue = 0f override var recalculate = true override var constrainTo: UIComponent? = null @@ -188,4 +206,11 @@ open class SiblingConstraint @JvmOverloads constructor( val index = component.parent.children.indexOf(component) return if (index == 0 && constrainTo == null) 0f else padding } + + @ApiStatus.Internal + @get:ApiStatus.Internal + override val managedStates = listOf( + ManagedState.OfFloat(paddingState, "padding", true), + ManagedState.OfBoolean(alignOppositeState, "alignOpposite", true), + ) } \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/constraints/debug/ConstraintDebugger.kt b/src/main/kotlin/gg/essential/elementa/constraints/debug/ConstraintDebugger.kt index 3f95b9f9..db09be5b 100644 --- a/src/main/kotlin/gg/essential/elementa/constraints/debug/ConstraintDebugger.kt +++ b/src/main/kotlin/gg/essential/elementa/constraints/debug/ConstraintDebugger.kt @@ -15,10 +15,10 @@ internal interface ConstraintDebugger { fun invokeImpl(constraint: SuperConstraint, type: ConstraintType, component: UIComponent): Float = when (type) { - ConstraintType.X -> (constraint as XConstraint).getXPositionImpl(component).roundToRealPixels() - ConstraintType.Y -> (constraint as YConstraint).getYPositionImpl(component).roundToRealPixels() - ConstraintType.WIDTH -> (constraint as WidthConstraint).getWidthImpl(component).roundToRealPixels() - ConstraintType.HEIGHT -> (constraint as HeightConstraint).getHeightImpl(component).roundToRealPixels() + ConstraintType.X -> (constraint as XConstraint).getXPositionImpl(component).roundToRealPixels(component) + ConstraintType.Y -> (constraint as YConstraint).getYPositionImpl(component).roundToRealPixels(component) + ConstraintType.WIDTH -> (constraint as WidthConstraint).getWidthImpl(component).roundToRealPixels(component) + ConstraintType.HEIGHT -> (constraint as HeightConstraint).getHeightImpl(component).roundToRealPixels(component) ConstraintType.RADIUS -> (constraint as RadiusConstraint).getRadiusImpl(component) else -> throw UnsupportedOperationException() } diff --git a/src/main/kotlin/gg/essential/elementa/debug/ExternalResolutionManager.kt b/src/main/kotlin/gg/essential/elementa/debug/ExternalResolutionManager.kt new file mode 100644 index 00000000..1786e16c --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/debug/ExternalResolutionManager.kt @@ -0,0 +1,31 @@ +package gg.essential.elementa.debug + +import gg.essential.elementa.impl.ExternalInspectorDisplay +import gg.essential.elementa.manager.ResolutionManager +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +class ExternalResolutionManager( + private val displayManager: ExternalInspectorDisplay, +) : ResolutionManager { + + override val windowWidth: Int + get() = displayManager.getWidth() + + override val windowHeight: Int + get() = displayManager.getHeight() + + override val viewportWidth: Int + get() = windowWidth + + override val viewportHeight: Int + get() = windowHeight + + override val scaledWidth: Int + get() = windowWidth / scaleFactor.toInt() + + override val scaledHeight: Int + get() = windowHeight / scaleFactor.toInt() + + override var scaleFactor: Double = 2.0 +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/debug/FrameBufferedWindow.kt b/src/main/kotlin/gg/essential/elementa/debug/FrameBufferedWindow.kt new file mode 100644 index 00000000..6919acc1 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/debug/FrameBufferedWindow.kt @@ -0,0 +1,161 @@ +package gg.essential.elementa.debug + +import gg.essential.elementa.components.Window +import gg.essential.elementa.impl.ExternalInspectorDisplay +import gg.essential.elementa.impl.Platform.Companion.platform +import gg.essential.elementa.manager.ResolutionManager +import gg.essential.universal.UGraphics +import gg.essential.universal.UMatrixStack +import org.jetbrains.annotations.ApiStatus +import org.lwjgl.opengl.GL11.* +import org.lwjgl.opengl.GL30 +import java.nio.ByteBuffer + +@ApiStatus.Internal +class FrameBufferedWindow( + private val wrappedWindow: Window, + private val externalDisplay: ExternalInspectorDisplay, +) { + + // Frame buffer properties + private var frameBuffer = -1 + private var frameBufferTexture = -1 + private var frameBufferWidth = -1 + private var frameBufferHeight = -1 + + + /** + * Called on the Minecraft thread update the buffer + */ + fun updateFrameBuffer(outerResolutionManager: ResolutionManager) { + + val frameWidth = externalDisplay.getWidth() + val frameHeight = externalDisplay.getHeight() + + // check if the frame width or height changed + if (frameWidth != frameBufferWidth || frameHeight != frameBufferHeight || frameBuffer == -1 || frameBufferTexture == -1) { + frameBufferWidth = frameWidth + frameBufferHeight = frameHeight + reconfigureFrameBuffer(frameWidth, frameHeight) + wrappedWindow.onWindowResize() + } + + withFrameBuffer(frameBuffer) { + + // Prepare frame buffer + val scissorState = glGetBoolean(GL_SCISSOR_TEST) + glDisable(GL_SCISSOR_TEST) + glClearColor(0f, 0f, 0f, 0f) + glClearDepth(1.0) + glClearStencil(0) + glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT) + glViewport(0, 0, frameWidth, frameHeight) + + // Undo MC's scaling and the distortion caused by different viewport size with same projection matrix + val stack = UMatrixStack() + val scale = wrappedWindow.resolutionManager.scaleFactor / outerResolutionManager.scaleFactor + stack.scale( + scale * outerResolutionManager.viewportWidth / frameWidth, + scale * outerResolutionManager.viewportHeight / frameHeight, + 1.0, + ) + + // Rendering + wrappedWindow.draw(stack) + + glViewport(0, 0, outerResolutionManager.viewportWidth, outerResolutionManager.viewportHeight) + + + if (scissorState) glEnable(GL_SCISSOR_TEST) + } + } + + + /** + * Sets up the frame buffer with the supplied [width] and [height]. + * If the frame buffer already exits, it will be deleted and recreated with the new size. + */ + private fun reconfigureFrameBuffer(width: Int, height: Int) { + deleteFrameBuffer() + frameBuffer = platform.genFrameBuffers() + frameBufferTexture = glGenTextures() + UGraphics.configureTexture(frameBufferTexture) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA, + width, + height, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + null as ByteBuffer? + ) + } + withFrameBuffer(frameBuffer) { + platform.framebufferTexture2D( + GL30.GL_FRAMEBUFFER, + GL30.GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, + frameBufferTexture, + 0 + ) + } + } + + /** + * Renders the contents of the frame buffer + */ + fun renderFrameBufferTexture() { + glEnable(GL_TEXTURE_2D) + glDisable(GL_CULL_FACE) + glBindTexture(GL_TEXTURE_2D, frameBufferTexture) + glBegin(GL_QUADS) + + glTexCoord2f(0f, 1f) + glVertex2f(0f, 0f) + + glTexCoord2f(1f, 1f) + glVertex2f(frameBufferWidth.toFloat(), 0f) + + glTexCoord2f(1f, 0f) + glVertex2f(frameBufferWidth.toFloat(), frameBufferHeight.toFloat()) + + glTexCoord2f(0f, 0f) + glVertex2f(0f, frameBufferHeight.toFloat()) + + glEnd() + } + + /** + * Runs the supplied [block] with the frame buffer bound + */ + private fun withFrameBuffer(glId: Int, block: Runnable) { + val prevReadFrameBufferBinding = glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING) + val prevDrawFrameBufferBinding = glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING) + + platform.bindFramebuffer(GL30.GL_FRAMEBUFFER, glId) + block.run() + + platform.bindFramebuffer(GL30.GL_READ_FRAMEBUFFER, prevReadFrameBufferBinding) + platform.bindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, prevDrawFrameBufferBinding) + } + + /** + * Deletes the frame buffer + */ + fun deleteFrameBuffer() { + if (frameBufferTexture != -1) { + glDeleteTextures(frameBufferTexture) + frameBufferTexture = -1 + } + + if (frameBuffer != -1) { + platform.deleteFramebuffers(frameBuffer) + frameBuffer = -1 + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/debug/StateRegistry.kt b/src/main/kotlin/gg/essential/elementa/debug/StateRegistry.kt new file mode 100644 index 00000000..91f5cf1f --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/debug/StateRegistry.kt @@ -0,0 +1,183 @@ +package gg.essential.elementa.debug + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.inspector.CompactToggle +import gg.essential.elementa.components.inspector.state.CompactSelector +import gg.essential.elementa.components.inspector.state.StateTextInput +import gg.essential.elementa.dsl.effect +import gg.essential.elementa.effects.OutlineEffect +import gg.essential.elementa.state.State +import gg.essential.elementa.utils.onLeftClick +import org.jetbrains.annotations.ApiStatus +import gg.essential.elementa.components.inspector.Inspector +import java.awt.Color + +/** + * Components and constraints can implement this interface and provide managed states through + * [managedStates]. Any managed state will have its value display in the [Inspector] + * and have its value configurable if it is mutable. + */ +internal interface StateRegistry { + + /** + * Returns a list of managed states that govern some aspect of this component or constraint. + */ + val managedStates: List +} + +/** + * Managed states are used to display and configure the value of a state inside the [Inspector]. + */ +@ApiStatus.Internal +sealed class ManagedState( + val name: String, + val mutable: Boolean, +) { + + @ApiStatus.Internal + class OfString(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfBoolean(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfColor(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfColorOrNull(val state: State, name: String, mutable: Boolean) : + ManagedState(name, mutable) + + @ApiStatus.Internal + class OfInt(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfFloat(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfDouble(val state: State, name: String, mutable: Boolean) : ManagedState(name, mutable) + + @ApiStatus.Internal + class OfEnum>(val state: State, name: String, mutable: Boolean) : + ManagedState(name, mutable) { + + fun createSelector(): UIComponent { + val values = state.get().javaClass.enumConstants.toList() + return CompactSelector(values, state) { + it.name + } + } + } + + @ApiStatus.Internal + class OfEnumerable( + val state: State, + val allValues: List, + val displayName: (T) -> String, + name: String, + mutable: Boolean, + ) : ManagedState(name, mutable) { + + fun createSelector(): UIComponent { + return CompactSelector(allValues, state) { + displayName(it) + } + } + } + +} + +@ApiStatus.Internal +object StateRegistryComponentFactory { + + fun createInspectorComponent(managedState: ManagedState): UIComponent { + return when (managedState) { + is ManagedState.OfFloat -> { + createInputComponent(managedState.state, managedState.mutable, { "%.2f".format(it) }) { + try { + it.toFloat() + } catch (e: NumberFormatException) { + throw StateTextInput.ParseException() + } + } + } + is ManagedState.OfDouble -> { + createInputComponent(managedState.state, managedState.mutable, { "%.2f".format(it) }) { + try { + it.toDouble() + } catch (e: NumberFormatException) { + throw StateTextInput.ParseException() + } + } + } + is ManagedState.OfBoolean -> { + CompactToggle(managedState.state).apply { + if (!managedState.mutable) { + mouseClickListeners.clear() // Disables the toggle + onLeftClick { + this effect OutlineEffect(Color.RED, 2f) + delay(250) { + this.effects.clear() + } + } + } + } + } + is ManagedState.OfInt -> { + createInputComponent(managedState.state, managedState.mutable, { it.toString() }) { + try { + it.toInt() + } catch (e: NumberFormatException) { + throw StateTextInput.ParseException() + } + } + } + is ManagedState.OfString -> { + createInputComponent(managedState.state, managedState.mutable, { it }) { + it + } + } + is ManagedState.OfColorOrNull -> { + createInputComponent( + managedState.state, + managedState.mutable, + { if (it == null) "null" else Integer.toHexString(it.rgb) }) { + if (it == "null" || it.isEmpty()) { + return@createInputComponent null + } + parseStringToColor(it) + } + } + is ManagedState.OfColor -> { + createInputComponent( + managedState.state, + managedState.mutable, + { Integer.toHexString(it.rgb) }) { + parseStringToColor(it) + } + } + is ManagedState.OfEnum<*> -> { + managedState.createSelector() + } + is ManagedState.OfEnumerable<*> -> { + managedState.createSelector() + } + } + } + + private fun parseStringToColor(string: String): Color { + return try { + Color(string.lowercase().toInt(16)) + } catch (e: NumberFormatException) { + throw StateTextInput.ParseException() + } + } + + private fun createInputComponent( + state: State, + mutable: Boolean, + formatToText: (T) -> String, + parse: (String) -> T, + ): UIComponent { + return StateTextInput(state, mutable, formatToText = formatToText, parse = parse) + } +} diff --git a/src/main/kotlin/gg/essential/elementa/effects/ScissorEffect.kt b/src/main/kotlin/gg/essential/elementa/effects/ScissorEffect.kt index 4e89efb6..51979066 100644 --- a/src/main/kotlin/gg/essential/elementa/effects/ScissorEffect.kt +++ b/src/main/kotlin/gg/essential/elementa/effects/ScissorEffect.kt @@ -1,9 +1,9 @@ package gg.essential.elementa.effects import gg.essential.elementa.UIComponent +import gg.essential.elementa.utils.resolutionManager import gg.essential.elementa.utils.roundToRealPixels import gg.essential.universal.UMatrixStack -import gg.essential.universal.UResolution import org.lwjgl.opengl.GL11.* import kotlin.math.max import kotlin.math.min @@ -17,12 +17,23 @@ import kotlin.math.roundToInt * * [scissorIntersection] will try to combine this scissor with all of it's parents scissors (if any). */ -class ScissorEffect @JvmOverloads constructor( +class ScissorEffect private constructor( private val customBoundingBox: UIComponent? = null, - private val scissorIntersection: Boolean = true + private val scissorIntersection: Boolean = true, + private val unroundedScissorBounds: ScissorBounds? = null, ) : Effect() { private var oldState: ScissorState? = null - private var scissorBounds: ScissorBounds? = null + private val roundedScissorBounds: ScissorBounds? by lazy { + if (unroundedScissorBounds == null) { + return@lazy null + } + ScissorBounds( + unroundedScissorBounds.x1.roundToRealPixels(boundComponent), + unroundedScissorBounds.y1.roundToRealPixels(boundComponent), + unroundedScissorBounds.x2.roundToRealPixels(boundComponent), + unroundedScissorBounds.y2.roundToRealPixels(boundComponent), + ) + } /** * Create a custom bounding box using precise coordinates. @@ -34,18 +45,30 @@ class ScissorEffect @JvmOverloads constructor( x2: Number, y2: Number, scissorIntersection: Boolean = true - ) : this(scissorIntersection = scissorIntersection) { - scissorBounds = ScissorBounds( - x1.toFloat().roundToRealPixels(), - y1.toFloat().roundToRealPixels(), - x2.toFloat().roundToRealPixels(), - y2.toFloat().roundToRealPixels(), - ) - } + ) : this( + scissorIntersection = scissorIntersection, + unroundedScissorBounds = ScissorBounds( + x1.toFloat(), + y1.toFloat(), + x2.toFloat(), + y2.toFloat(), + ), + ) + + @JvmOverloads + constructor( + customBoundingBox: UIComponent? = null, + scissorIntersection: Boolean = true, + ) : this( + customBoundingBox, + scissorIntersection, + null, + ) override fun beforeDraw(matrixStack: UMatrixStack) { - val bounds = customBoundingBox?.getScissorBounds() ?: scissorBounds ?: boundComponent.getScissorBounds() - val scaleFactor = UResolution.scaleFactor.toInt() + val bounds = customBoundingBox?.getScissorBounds() ?: roundedScissorBounds ?: boundComponent.getScissorBounds() + val resolutionManager = boundComponent.resolutionManager + val scaleFactor = resolutionManager.scaleFactor.toInt() if (currentScissorState == null) { glEnable(GL_SCISSOR_TEST) @@ -57,7 +80,7 @@ class ScissorEffect @JvmOverloads constructor( // TODO ideally we should respect matrixStack offset and maybe scale, though we do not currently care about // global gl state either, so not really important until someone needs it var x = (bounds.x1 * scaleFactor).roundToInt() - var y = UResolution.viewportHeight - (bounds.y2 * scaleFactor).roundToInt() + var y = resolutionManager.viewportHeight - (bounds.y2 * scaleFactor).roundToInt() var width = (bounds.width * scaleFactor).roundToInt() var height = (bounds.height * scaleFactor).roundToInt() @@ -96,10 +119,10 @@ class ScissorEffect @JvmOverloads constructor( } private fun UIComponent.getScissorBounds(): ScissorBounds = ScissorBounds( - getLeft().roundToRealPixels(), - getTop().roundToRealPixels(), - getRight().roundToRealPixels(), - getBottom().roundToRealPixels(), + getLeft().roundToRealPixels(this), + getTop().roundToRealPixels(this), + getRight().roundToRealPixels(this), + getBottom().roundToRealPixels(this), ) data class ScissorState(val x: Int, val y: Int, val width: Int, val height: Int) diff --git a/src/main/kotlin/gg/essential/elementa/font/FontRenderer.kt b/src/main/kotlin/gg/essential/elementa/font/FontRenderer.kt index a2dcf772..9510f8f5 100644 --- a/src/main/kotlin/gg/essential/elementa/font/FontRenderer.kt +++ b/src/main/kotlin/gg/essential/elementa/font/FontRenderer.kt @@ -1,6 +1,7 @@ package gg.essential.elementa.font import gg.essential.elementa.UIComponent +import gg.essential.elementa.components.Window import gg.essential.elementa.constraints.ConstraintType import gg.essential.elementa.constraints.resolution.ConstraintVisitor import gg.essential.elementa.font.data.Font @@ -8,7 +9,6 @@ import gg.essential.elementa.font.data.Glyph import gg.essential.elementa.utils.readFromLegacyShader import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack -import gg.essential.universal.UResolution import gg.essential.universal.shader.BlendState import gg.essential.universal.shader.Float2Uniform import gg.essential.universal.shader.Float4Uniform @@ -194,7 +194,7 @@ class FontRenderer( doffsetUniform.setValue(3.5f / currentPointSize) - val guiScale = UResolution.scaleFactor.toFloat() + val guiScale = Window.resolutionManager.scaleFactor.toFloat() //Reset obfuscated = false diff --git a/src/main/kotlin/gg/essential/elementa/impl/ExternalInspectorDisplay.kt b/src/main/kotlin/gg/essential/elementa/impl/ExternalInspectorDisplay.kt new file mode 100644 index 00000000..f17c21c6 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/impl/ExternalInspectorDisplay.kt @@ -0,0 +1,26 @@ +package gg.essential.elementa.impl + +import gg.essential.elementa.UIComponent +import gg.essential.elementa.manager.ResolutionManager +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface ExternalInspectorDisplay { + + val visible: Boolean + + fun updateVisiblity(visible: Boolean) + + fun addComponent(component: UIComponent) + + fun removeComponent(component: UIComponent) + + fun getWidth(): Int + + fun getHeight(): Int + + fun updateFrameBuffer(resolutionManager: ResolutionManager) + + fun cleanup() + +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/impl/Platform.kt b/src/main/kotlin/gg/essential/elementa/impl/Platform.kt index 9eec5117..dfad75ff 100644 --- a/src/main/kotlin/gg/essential/elementa/impl/Platform.kt +++ b/src/main/kotlin/gg/essential/elementa/impl/Platform.kt @@ -15,6 +15,16 @@ interface Platform { fun isCallingFromMinecraftThread(): Boolean + fun deleteFramebuffers(buffer: Int) + + fun genFrameBuffers(): Int + + fun framebufferTexture2D(targt: Int, attachment: Int, textarget: Int, texture: Int, level: Int) + + fun bindFramebuffer(target: Int, framebuffer: Int) + + fun runOnMinecraftThread(runnable: () -> Unit) + @ApiStatus.Internal companion object { internal val platform: Platform = diff --git a/src/main/kotlin/gg/essential/elementa/manager/DefaultKeyboardManager.kt b/src/main/kotlin/gg/essential/elementa/manager/DefaultKeyboardManager.kt new file mode 100644 index 00000000..1bc2434a --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/DefaultKeyboardManager.kt @@ -0,0 +1,17 @@ +package gg.essential.elementa.manager + +import gg.essential.universal.UKeyboard +import org.jetbrains.annotations.ApiStatus + +/** + * A keyboard manager that provides its values from [UKeyboard] + */ +@ApiStatus.Internal +object DefaultKeyboardManager : KeyboardManager { + + override fun isKeyDown(key: Int): Boolean = UKeyboard.isKeyDown(key) + + override fun allowRepeatEvents(enabled: Boolean) = UKeyboard.allowRepeatEvents(enabled) + + override fun getModifiers(): UKeyboard.Modifiers = UKeyboard.getModifiers() +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/manager/DefaultMousePositionManager.kt b/src/main/kotlin/gg/essential/elementa/manager/DefaultMousePositionManager.kt new file mode 100644 index 00000000..3f6a4287 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/DefaultMousePositionManager.kt @@ -0,0 +1,23 @@ +package gg.essential.elementa.manager + +import gg.essential.universal.UMouse +import org.jetbrains.annotations.ApiStatus + +/** + * A mouse position manager that provides its values from [UMouse] + */ +@ApiStatus.Internal +object DefaultMousePositionManager: MousePositionManager { + + override val rawX: Double + get() = UMouse.Raw.x + + override val rawY: Double + get() = UMouse.Raw.y + + override val scaledX: Double + get() = UMouse.Scaled.x + + override val scaledY: Double + get() = UMouse.Scaled.y +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/manager/DefaultResolutionManager.kt b/src/main/kotlin/gg/essential/elementa/manager/DefaultResolutionManager.kt new file mode 100644 index 00000000..523c45af --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/DefaultResolutionManager.kt @@ -0,0 +1,32 @@ +package gg.essential.elementa.manager + +import gg.essential.universal.UResolution +import org.jetbrains.annotations.ApiStatus + +/** + * A resolution manager that provides its values from [UResolution]. + */ +@ApiStatus.Internal +object DefaultResolutionManager : ResolutionManager { + + override val windowWidth: Int + get() = UResolution.windowWidth + + override val windowHeight: Int + get() = UResolution.windowHeight + + override val viewportWidth: Int + get() = UResolution.viewportWidth + + override val viewportHeight: Int + get() = UResolution.viewportHeight + + override val scaledWidth: Int + get() = UResolution.scaledWidth + + override val scaledHeight: Int + get() = UResolution.scaledHeight + + override val scaleFactor: Double + get() = UResolution.scaleFactor +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/manager/KeyboardManager.kt b/src/main/kotlin/gg/essential/elementa/manager/KeyboardManager.kt new file mode 100644 index 00000000..e66a7b25 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/KeyboardManager.kt @@ -0,0 +1,70 @@ +package gg.essential.elementa.manager + +import gg.essential.universal.UKeyboard +import gg.essential.universal.UMinecraft +import org.jetbrains.annotations.ApiStatus + +/** + * Provides a non-global way to access keyboard states. + */ +@ApiStatus.Internal +interface KeyboardManager { + + /** + * returns true if the supplied key with the supplied keycode is currently pressed. + */ + fun isKeyDown(key: Int): Boolean + + /** + * Sets whether repeat key events are enabled or not. + */ + fun allowRepeatEvents(enabled: Boolean) + + /** + * Gets the current state of current modifier keys + */ + fun getModifiers(): UKeyboard.Modifiers + + /* Default utility functions that may be overridden */ + + @ApiStatus.Internal + fun isShiftKeyDown(): Boolean = isKeyDown(UKeyboard.KEY_LSHIFT) || isKeyDown(UKeyboard.KEY_RSHIFT) + + @ApiStatus.Internal + fun isAltKeyDown(): Boolean = isKeyDown(UKeyboard.KEY_LMENU) || isKeyDown(UKeyboard.KEY_RMENU) + + @ApiStatus.Internal + fun isCtrlKeyDown(): Boolean = if (UMinecraft.isRunningOnMac) { + isKeyDown(UKeyboard.KEY_LMETA) || isKeyDown(UKeyboard.KEY_RMETA) + } else { + isKeyDown(UKeyboard.KEY_LCONTROL) || isKeyDown(UKeyboard.KEY_RCONTROL) + } + + @ApiStatus.Internal + fun isKeyComboCtrlA(key: Int): Boolean = + key == UKeyboard.KEY_A && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlC(key: Int): Boolean = + key == UKeyboard.KEY_C && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlV(key: Int): Boolean = + key == UKeyboard.KEY_V && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlX(key: Int): Boolean = + key == UKeyboard.KEY_X && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlY(key: Int): Boolean = + key == UKeyboard.KEY_Y && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlZ(key: Int): Boolean = + key == UKeyboard.KEY_Z && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown() + + @ApiStatus.Internal + fun isKeyComboCtrlShiftZ(key: Int): Boolean = + key == UKeyboard.KEY_Z && isCtrlKeyDown() && isShiftKeyDown() && !isAltKeyDown() +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/manager/MousePositionManager.kt b/src/main/kotlin/gg/essential/elementa/manager/MousePositionManager.kt new file mode 100644 index 00000000..e4e1a269 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/MousePositionManager.kt @@ -0,0 +1,19 @@ +package gg.essential.elementa.manager + +import org.jetbrains.annotations.ApiStatus + + +/** + * Provides a non-global way to access the cursor position. + */ +@ApiStatus.Internal +interface MousePositionManager { + + val rawX: Double + + val rawY: Double + + val scaledX: Double + + val scaledY: Double +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/manager/ResolutionManager.kt b/src/main/kotlin/gg/essential/elementa/manager/ResolutionManager.kt new file mode 100644 index 00000000..28262cb6 --- /dev/null +++ b/src/main/kotlin/gg/essential/elementa/manager/ResolutionManager.kt @@ -0,0 +1,25 @@ +package gg.essential.elementa.manager + +import org.jetbrains.annotations.ApiStatus + + +/** + * Provides a non-global way to access different aspects about the current resolution + */ +@ApiStatus.Internal +interface ResolutionManager { + + val windowWidth: Int + + val windowHeight: Int + + val viewportWidth: Int + + val viewportHeight: Int + + val scaledWidth: Int + + val scaledHeight: Int + + val scaleFactor: Double +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt b/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt index 872ff0c9..b6639508 100644 --- a/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt +++ b/src/main/kotlin/gg/essential/elementa/markdown/MarkdownComponent.kt @@ -16,6 +16,7 @@ import gg.essential.elementa.font.ElementaFonts import gg.essential.elementa.font.FontProvider import gg.essential.elementa.markdown.drawables.* import gg.essential.elementa.utils.elementaDebug +import gg.essential.elementa.utils.keyboardManager import gg.essential.universal.UDesktop import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack @@ -108,8 +109,8 @@ class MarkdownComponent( } onKeyType { _, keyCode -> - if (selection != null && keyCode == UKeyboard.KEY_C && UKeyboard.isCtrlKeyDown()) { - UDesktop.setClipboardString(drawables.selectedText(UKeyboard.isShiftKeyDown())) + if (selection != null && keyCode == UKeyboard.KEY_C && keyboardManager.isCtrlKeyDown()) { + UDesktop.setClipboardString(drawables.selectedText(keyboardManager.isShiftKeyDown())) } } } diff --git a/src/main/kotlin/gg/essential/elementa/markdown/drawables/TextDrawable.kt b/src/main/kotlin/gg/essential/elementa/markdown/drawables/TextDrawable.kt index a4291eec..deab8721 100644 --- a/src/main/kotlin/gg/essential/elementa/markdown/drawables/TextDrawable.kt +++ b/src/main/kotlin/gg/essential/elementa/markdown/drawables/TextDrawable.kt @@ -9,6 +9,7 @@ import gg.essential.elementa.markdown.HeaderLevelConfig import gg.essential.elementa.markdown.MarkdownComponent import gg.essential.elementa.markdown.MarkdownConfig import gg.essential.elementa.markdown.selection.TextCursor +import gg.essential.elementa.utils.mousePositionManager import gg.essential.universal.UMatrixStack import gg.essential.universal.UMouse import java.awt.Color @@ -176,8 +177,8 @@ class TextDrawable( } } - val mouseX = UMouse.Scaled.x - state.xShift - val mouseY = UMouse.Scaled.y - state.yShift + val mouseX = md.mousePositionManager.scaledX - state.xShift + val mouseY = md.mousePositionManager.scaledY - state.yShift isHovered = if (style.linkLocation != null) { isHovered(mouseX.toFloat(), mouseY.toFloat()) } else false diff --git a/src/main/kotlin/gg/essential/elementa/utils/extensions.kt b/src/main/kotlin/gg/essential/elementa/utils/extensions.kt index 3f33265d..facb0da2 100644 --- a/src/main/kotlin/gg/essential/elementa/utils/extensions.kt +++ b/src/main/kotlin/gg/essential/elementa/utils/extensions.kt @@ -2,22 +2,57 @@ package gg.essential.elementa.utils import gg.essential.elementa.UIComponent import gg.essential.elementa.components.Window +import gg.essential.elementa.effects.Effect +import gg.essential.elementa.events.UIClickEvent +import gg.essential.elementa.manager.* +import gg.essential.elementa.manager.DefaultMousePositionManager +import gg.essential.elementa.manager.DefaultResolutionManager +import gg.essential.elementa.manager.KeyboardManager +import gg.essential.elementa.manager.MousePositionManager +import gg.essential.elementa.manager.ResolutionManager +import gg.essential.elementa.state.BasicState +import gg.essential.elementa.state.State +import gg.essential.universal.UMouse import gg.essential.universal.UResolution import gg.essential.universal.shader.BlendState import gg.essential.universal.shader.UShader +import org.jetbrains.annotations.ApiStatus import java.awt.Color import kotlin.math.round import kotlin.math.sign +import kotlin.reflect.KProperty +//@Deprecated("This relies on global states", replaceWith = ReplaceWith("guiHint(roundDown, component)")) +@Suppress("DEPRECATION") fun Float.guiHint(roundDown: Boolean) = UIComponent.guiHint(this, roundDown) + +//@Deprecated("This relies on global states", replaceWith = ReplaceWith("guiHint(roundDown, component)")) +@Suppress("DEPRECATION") fun Double.guiHint(roundDown: Boolean) = UIComponent.guiHint(this, roundDown) +fun Float.guiHint(roundDown: Boolean, component: UIComponent) = UIComponent.guiHint(this, roundDown, component) + +fun Double.guiHint(roundDown: Boolean, component: UIComponent) = UIComponent.guiHint(this, roundDown, component) + +//@Deprecated("This relies on global states", replaceWith = ReplaceWith("roundToRealPixels(component)")) fun Float.roundToRealPixels(): Float { - val factor = UResolution.scaleFactor.toFloat() + val factor = Window.resolutionManager.scaleFactor.toFloat() + return round(this * factor).let { if (it == 0f && this != 0f) sign(this) else it } / factor +} + +fun Float.roundToRealPixels(component: UIComponent): Float { + val factor = component.resolutionManager.scaleFactor.toFloat() return round(this * factor).let { if (it == 0f && this != 0f) sign(this) else it } / factor } + +//@Deprecated("This relies on global states", replaceWith = ReplaceWith("roundToRealPixels(component)")) fun Double.roundToRealPixels(): Double { - val factor = UResolution.scaleFactor + val factor = Window.resolutionManager.scaleFactor + return round(this * factor).let { if (it == 0.0 && this != 0.0) sign(this) else it } / factor +} + +fun Double.roundToRealPixels(component: UIComponent): Double { + val factor = component.resolutionManager.scaleFactor return round(this * factor).let { if (it == 0.0 && this != 0.0) sign(this) else it } / factor } @@ -34,3 +69,183 @@ internal fun UShader.Companion.readFromLegacyShader(vertName: String, fragName: fromLegacyShader(readShader(vertName, "vsh"), readShader(fragName, "fsh"), blendState) private fun readShader(name: String, ext: String) = Window::class.java.getResource("/shaders/$name.$ext").readText() + +val UIComponent.window: Window? + get() = Window.ofOrNull(this) + +internal val UIComponent.resolutionManager: ResolutionManager + get() = window?.resolutionManager ?: DefaultResolutionManager + +internal val UIComponent.mousePositionManager: MousePositionManager + get() = window?.mousePositionManager ?: DefaultMousePositionManager + +internal val UIComponent.keyboardManager: KeyboardManager + get() = window?.keyboardManager ?: DefaultKeyboardManager + +inline fun UIComponent.onLeftClick(crossinline method: UIComponent.(event: UIClickEvent) -> Unit) = onMouseClick { + if (it.mouseButton == 0) { + this.method(it) + } +} + +fun State.onSetValueAndNow(listener: (T) -> Unit) = onSetValue(listener).also { listener(get()) } + +operator fun State.getValue(obj: Any, property: KProperty<*>): T = get() +operator fun State.setValue(obj: Any, property: KProperty<*>, value: T) = set(value) + +operator fun State.not() = map { !it } +infix fun State.and(other: State) = zip(other).map { (a, b) -> a && b } +infix fun State.or(other: State) = zip(other).map { (a, b) -> a || b } + +@ApiStatus.Internal +fun T.bindParent( + parent: UIComponent, + state: State, + delayed: Boolean = false, + index: Int? = null +) = + bindParent(state.map { + if (it) parent else null + }, delayed, index) + +@ApiStatus.Internal +fun T.bindParent(state: State, delayed: Boolean = false, index: Int? = null) = apply { + state.onSetValueAndNow { parent -> + val handleStateUpdate = { + if (this.hasParent && this.parent != parent) { + this.parent.removeChild(this) + } + if (parent != null && this !in parent.children) { + if (index != null) { + parent.insertChildAt(this, index) + } else { + parent.addChild(this) + } + } + } + if (delayed) { + Window.enqueueRenderOperation { + handleStateUpdate() + } + } else { + handleStateUpdate() + } + } +} + +/** + * Executes the supplied [block] on this component's animationFrame + */ +private fun UIComponent.onAnimationFrame(block: () -> Unit) = + enableEffect(object : Effect() { + override fun animationFrame() { + block() + } + }) + + +/** + * Returns a state representing whether this UIComponent is hovered + * + * [hitTest] will perform a hit test to make sure the user is actually hovered over this component + * as compared to the mouse just being within its content bounds while being hovered over another + * component rendered above this. + * + * [layoutSafe] will delay the state change until a time in which it is safe to make layout changes. + * This option will induce an additional delay of one frame because the state is updated during the next + * [Window.enqueueRenderOperation] after the hoverState changes. + */ +fun UIComponent.hoveredState(hitTest: Boolean = true, layoutSafe: Boolean = true): State { + // "Unsafe" means that it is not safe to depend on this for layout changes + val unsafeHovered = BasicState(false) + + // "Safe" because layout changes can directly happen when this changes (ie in onSetValue) + val safeHovered = BasicState(false) + + // Performs a hit test based on the current mouse x / y + fun hitTestHovered(): Boolean { + // Positions the mouse in the center of pixels so isPointInside will + // pass for items 1 pixel wide objects. See ElementaVersion v2 for more details + val halfPixel = 0.5f / UResolution.scaleFactor.toFloat() + val mouseX = mousePositionManager.scaledX.toFloat() + halfPixel + val mouseY = mousePositionManager.scaledY.toFloat() + halfPixel + return if (isPointInside(mouseX, mouseY)) { + + val window = Window.of(this) + val hit = (window.hoveredFloatingComponent?.hitTest(mouseX, mouseY)) ?: window.hitTest(mouseX, mouseY) + + hit.isComponentInParentChain(this) || hit == this + } else { + false + } + } + + if (hitTest) { + // It's possible the animation framerate will exceed that of the actual frame rate + // Therefore, in order to avoid redundantly performing the hit test multiple times + // in the same frame, this boolean is used to ensure that hit testing is performed + // at most only a single time each frame + var registerHitTest = true + + onAnimationFrame { + if (registerHitTest) { + registerHitTest = false + Window.enqueueRenderOperation { + // The next animation frame should register another renderOperation + registerHitTest = true + + // It is possible that this component or a component in its parent tree + // was removed from the component tree between the last call to animationFrame + // and this evaluation in enqueueRenderOperation. If that is the case, we should not + // perform the hit test because it will throw an exception. + if (!this.isInComponentTree()) { + // Unset the hovered state because a component can no longer + // be hovered if it is not in the component tree + unsafeHovered.set(false) + return@enqueueRenderOperation + } + + // Since enqueueRenderOperation will keep polling the queue until there are no more items, + // the forwarding of any update to the safeHovered state will still happen this frame + unsafeHovered.set(hitTestHovered()) + } + } + } + } + onMouseEnter { + if (hitTest) { + unsafeHovered.set(hitTestHovered()) + } else { + unsafeHovered.set(true) + } + } + + onMouseLeave { + unsafeHovered.set(false) + } + + return if (layoutSafe) { + unsafeHovered.onSetValue { + Window.enqueueRenderOperation { + safeHovered.set(it) + } + } + safeHovered + } else { + unsafeHovered + } +} + +fun UIComponent.isInComponentTree(): Boolean = + this is Window || hasParent && this in parent.children && parent.isInComponentTree() + +private fun UIComponent.isComponentInParentChain(target: UIComponent): Boolean { + var component: UIComponent = this + while (component.hasParent && component !is Window) { + component = component.parent + if (component == target) + return true + } + + return false +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/elementa/utils/options.kt b/src/main/kotlin/gg/essential/elementa/utils/options.kt index 32027ab4..c158476a 100644 --- a/src/main/kotlin/gg/essential/elementa/utils/options.kt +++ b/src/main/kotlin/gg/essential/elementa/utils/options.kt @@ -11,8 +11,3 @@ var elementaDev: Boolean = devPropSet } var elementaDebug: Boolean = debugPropSet - set(value) { - if (debugPropSet) { - field = value - } - } diff --git a/versions/src/main/java/gg/essential/elementa/impl/PlatformImpl.java b/versions/src/main/java/gg/essential/elementa/impl/PlatformImpl.java index 272ef877..54bdd239 100644 --- a/versions/src/main/java/gg/essential/elementa/impl/PlatformImpl.java +++ b/versions/src/main/java/gg/essential/elementa/impl/PlatformImpl.java @@ -1,12 +1,30 @@ package gg.essential.elementa.impl; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.shader.Framebuffer; import net.minecraft.util.ChatAllowedCharacters; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +//#if MC>=11700 +//$$ import static org.lwjgl.opengl.GL30.glBindFramebuffer; +//$$ import static org.lwjgl.opengl.GL30.glDeleteFramebuffers; +//$$ import static org.lwjgl.opengl.GL30.glFramebufferTexture2D; +//$$ import static org.lwjgl.opengl.GL30.glGenFramebuffers; +//#elseif MC>=11400 +//$$ import com.mojang.blaze3d.platform.GlStateManager; +//#else +import static net.minecraft.client.renderer.OpenGlHelper.glBindFramebuffer; +import static net.minecraft.client.renderer.OpenGlHelper.glDeleteFramebuffers; +import static net.minecraft.client.renderer.OpenGlHelper.glFramebufferTexture2D; +import static net.minecraft.client.renderer.OpenGlHelper.glGenFramebuffers; +//#endif + + @ApiStatus.Internal @SuppressWarnings("unused") // instantiated via reflection from Platform.Companion public class PlatformImpl implements Platform { @@ -62,4 +80,50 @@ public boolean isCallingFromMinecraftThread() { return Minecraft.getMinecraft().isCallingFromMinecraftThread(); //#endif } + + @Override + public void deleteFramebuffers(int buffer) { + //#if MC<=11202 || MC>=11700 + glDeleteFramebuffers(buffer); + //#else + //$$ GlStateManager.deleteFramebuffers(buffer); + //#endif + } + + @Override + public int genFrameBuffers() { + //#if MC<=11202 || MC>=11700 + return glGenFramebuffers(); + //#else + //$$ return GlStateManager.genFramebuffers(); + //#endif + } + + @Override + public void framebufferTexture2D(int target, int attachment, int textarget, int texture, int level) { + //#if MC<=11202 || MC>=11700 + glFramebufferTexture2D(target, attachment, textarget, texture, level); + //#else + //$$ GlStateManager.framebufferTexture2D(target, attachment, textarget, texture, level); + //#endif + } + + @Override + public void bindFramebuffer(int target, int framebuffer) { + //#if MC<=11202 || MC>=11700 + glBindFramebuffer(target, framebuffer); + //#else + //$$ GlStateManager.bindFramebuffer(target, framebuffer); + //#endif + } + + + @Override + public void runOnMinecraftThread(@NotNull Function0 runnable) { + //#if MC<=11202 + Minecraft.getMinecraft().addScheduledTask(runnable::invoke); + //#else + //$$ Minecraft.getInstance().execute(runnable::invoke); + //#endif + } }