diff --git a/GUI_SYSTEM.md b/GUI_SYSTEM.md
index 2b2f026..231864f 100644
--- a/GUI_SYSTEM.md
+++ b/GUI_SYSTEM.md
@@ -10,6 +10,8 @@ AmbleKit provides a declarative JSON-based GUI system that lets you create Minec
- [Background Types](#background-types)
- [Text Elements](#text-elements)
- [Text Input Elements](#text-input-elements)
+- [Slider Elements](#slider-elements)
+- [Color Picker Elements](#color-picker-elements)
- [Entity Display Elements](#entity-display-elements)
- [Buttons & Interactivity](#buttons--interactivity)
- [Lua Script Integration](#lua-script-integration)
@@ -328,6 +330,235 @@ end
---
+## Slider Elements
+
+Create interactive slider controls using the `slider` property. Sliders allow users to select a value within a configurable range by dragging a thumb along a track.
+
+### Basic Slider
+
+```json
+{
+ "id": "mymod:volume_slider",
+ "slider": true,
+ "min": 0,
+ "max": 100,
+ "value": 50,
+ "layout": [150, 20],
+ "background": [30, 30, 40]
+}
+```
+
+### Slider Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `slider` | boolean | required | Must be `true` to create a slider |
+| `min` | float | `0` | Minimum value |
+| `max` | float | `1` | Maximum value |
+| `value` | float | `0` | Initial value |
+| `step` | float | `0` | Step increment for snapping (0 = continuous) |
+
+### Visual Customization
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `track_color` | [r,g,b] or [r,g,b,a] | dark gray | Color of the unfilled track |
+| `track_filled_color` | [r,g,b] or [r,g,b,a] | blue | Color of the filled track portion |
+| `thumb_color` | [r,g,b] or [r,g,b,a] | light gray | Color of the thumb |
+| `thumb_hover_color` | [r,g,b] or [r,g,b,a] | white | Color of thumb when hovered |
+| `border_color` | [r,g,b] or [r,g,b,a] | gray | Border color when unfocused |
+| `focused_border_color` | [r,g,b] or [r,g,b,a] | light blue | Border color when focused |
+| `track_height` | integer | `4` | Height of the track in pixels |
+| `thumb_width` | integer | `8` | Width of the thumb in pixels |
+| `thumb_height` | integer | `16` | Height of the thumb in pixels |
+
+### Styled Slider Example
+
+```json
+{
+ "id": "mymod:brightness_slider",
+ "slider": true,
+ "min": 0,
+ "max": 100,
+ "value": 75,
+ "step": 5,
+ "layout": [200, 24],
+ "background": [20, 20, 30],
+ "track_color": [40, 40, 50],
+ "track_filled_color": [80, 140, 200],
+ "thumb_color": [180, 180, 200],
+ "thumb_hover_color": [220, 220, 255],
+ "border_color": [60, 60, 80],
+ "focused_border_color": [100, 140, 220],
+ "script": "mymod:brightness_handler"
+}
+```
+
+### Keyboard Controls
+
+| Key | Action |
+|-----|--------|
+| `←` / `↓` | Decrease value by step (or 1%) |
+| `→` / `↑` | Increase value by step (or 1%) |
+| `Ctrl+←` / `Ctrl+↓` | Decrease by 10× step |
+| `Ctrl+→` / `Ctrl+↑` | Increase by 10× step |
+| `Home` | Set to minimum value |
+| `End` | Set to maximum value |
+| `Tab` | Move focus to next element |
+
+### Reading Slider Values in Lua
+
+```lua
+function onValueChanged(self, value)
+ -- Called whenever the slider value changes
+ local mc = self:minecraft()
+ mc:sendMessage("Value changed to: " .. value, false)
+end
+
+function onClick(self, mouseX, mouseY, button)
+ local root = getRoot(self)
+ local slider = findById(root, "mymod:volume_slider")
+
+ if slider then
+ local value = slider:getValue()
+ local min = slider:getMin()
+ local max = slider:getMax()
+ end
+end
+```
+
+### LuaElement Slider API
+
+| Method | Description |
+|--------|-------------|
+| `self:getValue()` | Get the current value |
+| `self:setValue(value)` | Set the value (clamped to min/max) |
+| `self:getMin()` | Get the minimum value |
+| `self:setMin(min)` | Set the minimum value |
+| `self:getMax()` | Get the maximum value |
+| `self:setMax(max)` | Set the maximum value |
+| `self:getStep()` | Get the step increment |
+| `self:setStep(step)` | Set the step increment |
+
+---
+
+## Color Picker Elements
+
+Create interactive color pickers using the `color_picker` property. Color pickers display a small color swatch that expands into a full color selection popup with a hue bar, saturation/value square, and input fields.
+
+### Basic Color Picker
+
+```json
+{
+ "id": "mymod:text_color",
+ "color_picker": true,
+ "initial_color": [255, 128, 0],
+ "layout": [24, 24],
+ "background": [30, 30, 40]
+}
+```
+
+### Color Picker Properties
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `color_picker` | boolean | required | Must be `true` to create a color picker |
+| `initial_color` | [r,g,b] or [r,g,b,a] | white | Initial color value |
+| `include_alpha` | boolean | `false` | Whether to show an alpha slider |
+
+### Visual Customization
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `border_collapsed` | [r,g,b] or [r,g,b,a] | gray | Border color when collapsed |
+| `border_collapsed_hover` | [r,g,b] or [r,g,b,a] | light gray | Border color when hovered |
+| `border_expanded` | [r,g,b] or [r,g,b,a] | dark gray | Border color of expanded popup |
+| `background_expanded` | [r,g,b,a] | dark semi-transparent | Background of expanded popup |
+| `popup_width` | integer | `180` | Width of expanded popup in pixels |
+| `popup_height` | integer | `150` | Height of expanded popup in pixels |
+
+### Color Picker with Alpha
+
+```json
+{
+ "id": "mymod:highlight_color",
+ "color_picker": true,
+ "initial_color": [255, 255, 0, 128],
+ "include_alpha": true,
+ "layout": [30, 30],
+ "background": [25, 25, 35],
+ "border_collapsed": [70, 70, 90],
+ "border_collapsed_hover": [100, 100, 140],
+ "background_expanded": [35, 35, 50, 245],
+ "popup_width": 200,
+ "popup_height": 160
+}
+```
+
+### Expanded Popup Features
+
+When expanded, the color picker displays:
+
+1. **Saturation/Value Square** - Click and drag to select saturation (horizontal) and brightness (vertical)
+2. **Hue Bar** - Vertical rainbow bar for selecting the base hue
+3. **Alpha Bar** (optional) - Horizontal bar for selecting transparency
+4. **Hex Input** - Text field accepting `RRGGBB` or `RRGGBBAA` format
+5. **RGB/A Inputs** - Individual numeric fields for each color component (auto-clamps 0-255)
+
+### Keyboard Controls
+
+| Key | Action |
+|-----|--------|
+| `Escape` | Close the expanded popup |
+| `Enter` | Apply changes and close popup |
+| `Tab` | Cycle through input fields |
+| `Shift+Tab` | Cycle backwards through input fields |
+
+### Reading Color Values in Lua
+
+```lua
+function onColorChanged(self, r, g, b, a)
+ -- Called whenever the color changes
+ local hex = self:getColorHex()
+ local mc = self:minecraft()
+ mc:sendMessage("Color changed to #" .. hex, false)
+end
+
+function onClick(self, mouseX, mouseY, button)
+ local root = getRoot(self)
+ local picker = findById(root, "mymod:text_color")
+
+ if picker then
+ -- Get color as hex string
+ local hex = picker:getColorHex()
+
+ -- Get color as RGBA array
+ local rgba = picker:getColorRGBA()
+ local r, g, b, a = rgba[1], rgba[2], rgba[3], rgba[4]
+
+ -- Set color programmatically
+ picker:setColorRGBA(255, 0, 128, 255)
+ -- Or by hex
+ picker:setColorHex("FF0080")
+ end
+end
+```
+
+### LuaElement Color Picker API
+
+| Method | Description |
+|--------|-------------|
+| `self:getColorRGBA()` | Get color as `{r, g, b, a}` array (0-255) |
+| `self:setColorRGBA(r,g,b,a)` | Set color from RGBA values |
+| `self:getColorHex()` | Get color as hex string (`RRGGBB` or `RRGGBBAA`) |
+| `self:setColorHex(hex)` | Set color from hex string |
+| `self:isPickerExpanded()` | Check if popup is open |
+| `self:setPickerExpanded(bool)` | Open or close the popup |
+| `self:isIncludeAlpha()` | Check if alpha slider is shown |
+| `self:setIncludeAlpha(bool)` | Show/hide alpha slider |
+
+---
+
## Entity Display Elements
Display living entities within a GUI element using the `entity_uuid` property. This renders the entity in an inventory-screen style, perfect for character viewers, mob displays, or player previews.
diff --git a/src/main/java/dev/amble/lib/client/gui/AmbleColorPicker.java b/src/main/java/dev/amble/lib/client/gui/AmbleColorPicker.java
new file mode 100644
index 0000000..4b2ef9f
--- /dev/null
+++ b/src/main/java/dev/amble/lib/client/gui/AmbleColorPicker.java
@@ -0,0 +1,1001 @@
+package dev.amble.lib.client.gui;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import dev.amble.lib.AmbleKit;
+import dev.amble.lib.client.gui.lua.LuaElement;
+import dev.amble.lib.client.gui.registry.AmbleElementParser;
+import dev.amble.lib.script.LuaScript;
+import dev.amble.lib.script.ScriptManager;
+import dev.amble.lib.script.lua.LuaBinder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.Varargs;
+
+import java.awt.*;
+
+/**
+ * A color picker element with collapsed swatch and expanded picker overlay.
+ *
+ * Features:
+ *
+ * - Collapsed state shows a color swatch with configurable border
+ * - Expanded state overlays a popup with hue bar, SV square, and input fields
+ * - Hex input field (#RRGGBB or #RRGGBBAA format)
+ * - RGB numeric input fields (auto-clamp 0-255)
+ * - Optional alpha slider
+ * - Script callback for color changes
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class AmbleColorPicker extends AmbleContainer implements Focusable {
+
+ // Current color (stored as RGBA)
+ private int red = 255;
+ private int green = 255;
+ private int blue = 255;
+ private int alpha = 255;
+
+ // HSV representation for the picker
+ private float hue = 0f;
+ private float saturation = 0f;
+ private float brightness = 1f;
+
+ // Expanded state
+ @Setter
+ private boolean expanded = false;
+
+ // Focus state
+ @Setter
+ private boolean focused = false;
+
+ // Alpha support
+ @Setter
+ private boolean includeAlpha = false;
+
+ // Visual customization - collapsed state
+ @Setter
+ private Color borderCollapsed = new Color(100, 100, 100);
+ @Setter
+ private Color borderCollapsedHover = new Color(150, 150, 150);
+
+ // Visual customization - expanded state
+ @Setter
+ private Color borderExpanded = new Color(60, 60, 60);
+ @Setter
+ private Color backgroundExpanded = new Color(40, 40, 50, 240);
+
+ // Popup dimensions
+ @Setter
+ private int popupWidth = 180;
+ @Setter
+ private int popupHeight = 150;
+
+ // Interaction state
+ private boolean isHovered = false;
+ private boolean isDraggingHue = false;
+ private boolean isDraggingSV = false;
+ private boolean isDraggingAlpha = false;
+
+ // Input field state (simplified - we'll render inline)
+ private String hexInputText = "FFFFFF";
+ private String rInputText = "255";
+ private String gInputText = "255";
+ private String bInputText = "255";
+ private String aInputText = "255";
+
+ // Which input is focused (0=none, 1=hex, 2=r, 3=g, 4=b, 5=a)
+ private int focusedInput = 0;
+ private int cursorPosition = 0;
+ private long lastCursorBlink = 0;
+ private boolean cursorVisible = true;
+
+ // Layout constants
+ private static final int HUE_BAR_WIDTH = 16;
+ private static final int ALPHA_BAR_HEIGHT = 12;
+ private static final int INPUT_HEIGHT = 14;
+ private static final int LABEL_WIDTH = 14;
+ private static final int CURSOR_BLINK_RATE = 530;
+
+ // Script support
+ @Setter
+ private @Nullable LuaScript script;
+
+ /**
+ * Sets the color from RGBA values (0-255 each).
+ */
+ public void setColor(int r, int g, int b, int a) {
+ this.red = clamp(r, 0, 255);
+ this.green = clamp(g, 0, 255);
+ this.blue = clamp(b, 0, 255);
+ this.alpha = clamp(a, 0, 255);
+ updateHSVFromRGB();
+ updateInputTexts();
+ onColorChanged();
+ }
+
+ /**
+ * Sets the color from RGBA values (0-255 each) without triggering callback.
+ */
+ private void setColorInternal(int r, int g, int b, int a) {
+ this.red = clamp(r, 0, 255);
+ this.green = clamp(g, 0, 255);
+ this.blue = clamp(b, 0, 255);
+ this.alpha = clamp(a, 0, 255);
+ updateHSVFromRGB();
+ updateInputTexts();
+ }
+
+ /**
+ * Sets the color from a hex string (with or without #, with or without alpha).
+ */
+ public void setColorHex(String hex) {
+ hex = hex.replace("#", "");
+ try {
+ if (hex.length() == 6) {
+ int r = Integer.parseInt(hex.substring(0, 2), 16);
+ int g = Integer.parseInt(hex.substring(2, 4), 16);
+ int b = Integer.parseInt(hex.substring(4, 6), 16);
+ setColor(r, g, b, alpha);
+ } else if (hex.length() == 8) {
+ int r = Integer.parseInt(hex.substring(0, 2), 16);
+ int g = Integer.parseInt(hex.substring(2, 4), 16);
+ int b = Integer.parseInt(hex.substring(4, 6), 16);
+ int a = Integer.parseInt(hex.substring(6, 8), 16);
+ setColor(r, g, b, a);
+ }
+ } catch (NumberFormatException e) {
+ // Invalid hex, ignore
+ }
+ }
+
+ /**
+ * Gets the current color as a hex string.
+ */
+ public String getColorHex() {
+ if (includeAlpha) {
+ return String.format("%02X%02X%02X%02X", red, green, blue, alpha);
+ }
+ return String.format("%02X%02X%02X", red, green, blue);
+ }
+
+ /**
+ * Gets the current color as a java.awt.Color.
+ */
+ public Color getColor() {
+ return new Color(red, green, blue, alpha);
+ }
+
+ private void updateHSVFromRGB() {
+ float[] hsv = Color.RGBtoHSB(red, green, blue, null);
+ this.hue = hsv[0];
+ this.saturation = hsv[1];
+ this.brightness = hsv[2];
+ }
+
+ private void updateRGBFromHSV() {
+ int rgb = Color.HSBtoRGB(hue, saturation, brightness);
+ this.red = (rgb >> 16) & 0xFF;
+ this.green = (rgb >> 8) & 0xFF;
+ this.blue = rgb & 0xFF;
+ updateInputTexts();
+ }
+
+ private void updateInputTexts() {
+ hexInputText = String.format("%02X%02X%02X", red, green, blue);
+ rInputText = String.valueOf(red);
+ gInputText = String.valueOf(green);
+ bInputText = String.valueOf(blue);
+ aInputText = String.valueOf(alpha);
+ }
+
+ private static int clamp(int value, int min, int max) {
+ return Math.max(min, Math.min(max, value));
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ Rectangle layout = getLayout();
+ isHovered = isHovered(mouseX, mouseY);
+
+ // Render collapsed swatch
+ renderCollapsedSwatch(context, layout, mouseX, mouseY);
+
+ // Render expanded popup if open
+ if (expanded) {
+ renderExpandedPopup(context, layout, mouseX, mouseY, delta);
+ }
+
+ // Render children
+ for (AmbleElement child : getChildren()) {
+ if (child.isVisible()) {
+ child.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ private void renderCollapsedSwatch(DrawContext context, Rectangle layout, int mouseX, int mouseY) {
+ // Draw color swatch
+ context.fill(layout.x + 1, layout.y + 1, layout.x + layout.width - 1, layout.y + layout.height - 1,
+ new Color(red, green, blue, alpha).getRGB());
+
+ // Draw checkerboard pattern behind if alpha < 255 (to show transparency)
+ if (alpha < 255) {
+ renderCheckerboard(context, layout.x + 1, layout.y + 1, layout.width - 2, layout.height - 2);
+ // Re-draw color with alpha
+ context.fill(layout.x + 1, layout.y + 1, layout.x + layout.width - 1, layout.y + layout.height - 1,
+ new Color(red, green, blue, alpha).getRGB());
+ }
+
+ // Draw border
+ Color border = (isHovered || expanded) ? borderCollapsedHover : borderCollapsed;
+ drawBorder(context, layout.x, layout.y, layout.width, layout.height, border);
+ }
+
+ private void renderExpandedPopup(DrawContext context, Rectangle layout, int mouseX, int mouseY, float delta) {
+ // Calculate popup position (below the swatch)
+ int popupX = layout.x;
+ int popupY = layout.y + layout.height + 2;
+
+ // Calculate actual popup height based on whether alpha is included
+ int actualPopupHeight = popupHeight + (includeAlpha ? ALPHA_BAR_HEIGHT + 4 : 0);
+
+ // Draw popup background
+ context.fill(popupX, popupY, popupX + popupWidth, popupY + actualPopupHeight, backgroundExpanded.getRGB());
+ drawBorder(context, popupX, popupY, popupWidth, actualPopupHeight, borderExpanded);
+
+ int padding = 4;
+ int innerX = popupX + padding;
+ int innerY = popupY + padding;
+ int innerWidth = popupWidth - padding * 2;
+
+ // Calculate SV square dimensions
+ int svSize = innerWidth - HUE_BAR_WIDTH - 4;
+ int svX = innerX;
+ int svY = innerY;
+
+ // Render SV square
+ renderSVSquare(context, svX, svY, svSize, mouseX, mouseY);
+
+ // Render hue bar
+ int hueX = svX + svSize + 4;
+ int hueY = innerY;
+ int hueHeight = svSize;
+ renderHueBar(context, hueX, hueY, HUE_BAR_WIDTH, hueHeight, mouseX, mouseY);
+
+ // Calculate input area Y position
+ int inputY = svY + svSize + 4;
+
+ // Render alpha bar if enabled
+ if (includeAlpha) {
+ renderAlphaBar(context, innerX, inputY, innerWidth, ALPHA_BAR_HEIGHT, mouseX, mouseY);
+ inputY += ALPHA_BAR_HEIGHT + 4;
+ }
+
+ // Render input fields
+ renderInputFields(context, innerX, inputY, innerWidth, mouseX, mouseY, delta);
+ }
+
+ private void renderSVSquare(DrawContext context, int x, int y, int size, int mouseX, int mouseY) {
+ // Draw the SV gradient
+ for (int py = 0; py < size; py++) {
+ for (int px = 0; px < size; px++) {
+ float s = (float) px / size;
+ float v = 1.0f - (float) py / size;
+ int rgb = Color.HSBtoRGB(hue, s, v);
+ context.fill(x + px, y + py, x + px + 1, y + py + 1, rgb | 0xFF000000);
+ }
+ }
+
+ // Draw border
+ drawBorder(context, x, y, size, size, new Color(80, 80, 80));
+
+ // Draw crosshair at current position
+ int crossX = x + (int) (saturation * size);
+ int crossY = y + (int) ((1 - brightness) * size);
+ int crossSize = 4;
+
+ // White outline
+ context.fill(crossX - crossSize - 1, crossY, crossX + crossSize + 2, crossY + 1, 0xFFFFFFFF);
+ context.fill(crossX, crossY - crossSize - 1, crossX + 1, crossY + crossSize + 2, 0xFFFFFFFF);
+ // Black center
+ context.fill(crossX - crossSize, crossY, crossX + crossSize + 1, crossY + 1, 0xFF000000);
+ context.fill(crossX, crossY - crossSize, crossX + 1, crossY + crossSize + 1, 0xFF000000);
+ }
+
+ private void renderHueBar(DrawContext context, int x, int y, int width, int height, int mouseX, int mouseY) {
+ // Draw hue gradient (vertical rainbow)
+ for (int py = 0; py < height; py++) {
+ float h = (float) py / height;
+ int rgb = Color.HSBtoRGB(h, 1f, 1f);
+ context.fill(x, y + py, x + width, y + py + 1, rgb | 0xFF000000);
+ }
+
+ // Draw border
+ drawBorder(context, x, y, width, height, new Color(80, 80, 80));
+
+ // Draw selector at current hue
+ int selectorY = y + (int) (hue * height);
+ context.fill(x - 1, selectorY - 1, x + width + 1, selectorY + 2, 0xFFFFFFFF);
+ context.fill(x, selectorY, x + width, selectorY + 1, 0xFF000000);
+ }
+
+ private void renderAlphaBar(DrawContext context, int x, int y, int width, int height, int mouseX, int mouseY) {
+ // Draw checkerboard background
+ renderCheckerboard(context, x, y, width, height);
+
+ // Draw alpha gradient
+ for (int px = 0; px < width; px++) {
+ int a = (int) ((float) px / width * 255);
+ int color = (a << 24) | (red << 16) | (green << 8) | blue;
+ context.fill(x + px, y, x + px + 1, y + height, color);
+ }
+
+ // Draw border
+ drawBorder(context, x, y, width, height, new Color(80, 80, 80));
+
+ // Draw selector at current alpha
+ int selectorX = x + (int) ((float) alpha / 255 * width);
+ context.fill(selectorX - 1, y - 1, selectorX + 2, y + height + 1, 0xFFFFFFFF);
+ context.fill(selectorX, y, selectorX + 1, y + height, 0xFF000000);
+ }
+
+ private void renderCheckerboard(DrawContext context, int x, int y, int width, int height) {
+ int checkSize = 4;
+ for (int py = 0; py < height; py += checkSize) {
+ for (int px = 0; px < width; px += checkSize) {
+ boolean isLight = ((px / checkSize) + (py / checkSize)) % 2 == 0;
+ int color = isLight ? 0xFFCCCCCC : 0xFF999999;
+ int x2 = Math.min(x + px + checkSize, x + width);
+ int y2 = Math.min(y + py + checkSize, y + height);
+ context.fill(x + px, y + py, x2, y2, color);
+ }
+ }
+ }
+
+ private void renderInputFields(DrawContext context, int x, int y, int width, int mouseX, int mouseY, float delta) {
+ TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer;
+ int fieldHeight = INPUT_HEIGHT;
+ int spacing = 2;
+
+ // Update cursor blink
+ long now = System.currentTimeMillis();
+ if (now - lastCursorBlink > CURSOR_BLINK_RATE) {
+ cursorVisible = !cursorVisible;
+ lastCursorBlink = now;
+ }
+
+ // Hex input (top row)
+ int hexWidth = width;
+ renderInputField(context, textRenderer, x, y, hexWidth, fieldHeight, "#", hexInputText, focusedInput == 1, mouseX, mouseY);
+
+ // RGB inputs (bottom row)
+ int rgbY = y + fieldHeight + spacing;
+ int fieldWidth = (width - spacing * 2 - (includeAlpha ? spacing : 0)) / (includeAlpha ? 4 : 3);
+
+ renderInputField(context, textRenderer, x, rgbY, fieldWidth, fieldHeight, "R", rInputText, focusedInput == 2, mouseX, mouseY);
+ renderInputField(context, textRenderer, x + fieldWidth + spacing, rgbY, fieldWidth, fieldHeight, "G", gInputText, focusedInput == 3, mouseX, mouseY);
+ renderInputField(context, textRenderer, x + (fieldWidth + spacing) * 2, rgbY, fieldWidth, fieldHeight, "B", bInputText, focusedInput == 4, mouseX, mouseY);
+
+ if (includeAlpha) {
+ renderInputField(context, textRenderer, x + (fieldWidth + spacing) * 3, rgbY, fieldWidth, fieldHeight, "A", aInputText, focusedInput == 5, mouseX, mouseY);
+ }
+ }
+
+ private void renderInputField(DrawContext context, TextRenderer textRenderer, int x, int y, int width, int height,
+ String label, String text, boolean isFocused, int mouseX, int mouseY) {
+ // Background
+ context.fill(x, y, x + width, y + height, isFocused ? 0xFF303040 : 0xFF252530);
+
+ // Border
+ Color borderColor = isFocused ? new Color(80, 140, 200) : new Color(60, 60, 70);
+ drawBorder(context, x, y, width, height, borderColor);
+
+ // Label
+ int labelX = x + 2;
+ int textY = y + (height - textRenderer.fontHeight) / 2;
+ context.drawText(textRenderer, label, labelX, textY, 0xFF808090, false);
+
+ // Text
+ int textX = labelX + textRenderer.getWidth(label) + 2;
+ int maxTextWidth = width - (textX - x) - 2;
+
+ // Scissor to prevent text overflow
+ context.enableScissor(textX, y, x + width - 2, y + height);
+ context.drawText(textRenderer, text, textX, textY, 0xFFFFFFFF, false);
+
+ // Cursor
+ if (isFocused && cursorVisible) {
+ int cursorX = textX + textRenderer.getWidth(text.substring(0, Math.min(cursorPosition, text.length())));
+ context.fill(cursorX, textY - 1, cursorX + 1, textY + textRenderer.fontHeight + 1, 0xFFFFFFFF);
+ }
+
+ context.disableScissor();
+ }
+
+ private void drawBorder(DrawContext context, int x, int y, int w, int h, Color color) {
+ int c = color.getRGB();
+ context.fill(x, y, x + w, y + 1, c);
+ context.fill(x, y + h - 1, x + w, y + h, c);
+ context.fill(x, y, x + 1, y + h, c);
+ context.fill(x + w - 1, y, x + w, y + h, c);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY, int button) {
+ if (button != 0) {
+ super.onClick(mouseX, mouseY, button);
+ return;
+ }
+
+ Rectangle layout = getLayout();
+
+ // Check if clicking on collapsed swatch
+ if (!expanded && isHovered(mouseX, mouseY)) {
+ expanded = true;
+ focusedInput = 0;
+ return;
+ }
+
+ // Check if clicking outside expanded popup
+ if (expanded) {
+ int popupX = layout.x;
+ int popupY = layout.y + layout.height + 2;
+ int actualPopupHeight = popupHeight + (includeAlpha ? ALPHA_BAR_HEIGHT + 4 : 0);
+
+ boolean inSwatch = mouseX >= layout.x && mouseX <= layout.x + layout.width &&
+ mouseY >= layout.y && mouseY <= layout.y + layout.height;
+ boolean inPopup = mouseX >= popupX && mouseX <= popupX + popupWidth &&
+ mouseY >= popupY && mouseY <= popupY + actualPopupHeight;
+
+ if (!inSwatch && !inPopup) {
+ expanded = false;
+ focusedInput = 0;
+ applyInputTexts();
+ return;
+ }
+
+ // Handle clicks within popup
+ if (inPopup) {
+ handlePopupClick(mouseX, mouseY, popupX, popupY);
+ }
+ }
+
+ super.onClick(mouseX, mouseY, button);
+ }
+
+ private void handlePopupClick(double mouseX, double mouseY, int popupX, int popupY) {
+ int padding = 4;
+ int innerX = popupX + padding;
+ int innerY = popupY + padding;
+ int innerWidth = popupWidth - padding * 2;
+ int svSize = innerWidth - HUE_BAR_WIDTH - 4;
+
+ // Check SV square
+ if (mouseX >= innerX && mouseX < innerX + svSize &&
+ mouseY >= innerY && mouseY < innerY + svSize) {
+ isDraggingSV = true;
+ focusedInput = 0;
+ updateSVFromMouse(mouseX, mouseY, innerX, innerY, svSize);
+ return;
+ }
+
+ // Check hue bar
+ int hueX = innerX + svSize + 4;
+ if (mouseX >= hueX && mouseX < hueX + HUE_BAR_WIDTH &&
+ mouseY >= innerY && mouseY < innerY + svSize) {
+ isDraggingHue = true;
+ focusedInput = 0;
+ updateHueFromMouse(mouseY, innerY, svSize);
+ return;
+ }
+
+ // Check alpha bar
+ int inputY = innerY + svSize + 4;
+ if (includeAlpha) {
+ if (mouseX >= innerX && mouseX < innerX + innerWidth &&
+ mouseY >= inputY && mouseY < inputY + ALPHA_BAR_HEIGHT) {
+ isDraggingAlpha = true;
+ focusedInput = 0;
+ updateAlphaFromMouse(mouseX, innerX, innerWidth);
+ return;
+ }
+ inputY += ALPHA_BAR_HEIGHT + 4;
+ }
+
+ // Check input fields
+ handleInputFieldClick(mouseX, mouseY, innerX, inputY, innerWidth);
+ }
+
+ private void handleInputFieldClick(double mouseX, double mouseY, int x, int y, int width) {
+ int fieldHeight = INPUT_HEIGHT;
+ int spacing = 2;
+
+ // Hex field
+ if (mouseY >= y && mouseY < y + fieldHeight) {
+ focusedInput = 1;
+ cursorPosition = hexInputText.length();
+ resetCursorBlink();
+ return;
+ }
+
+ // RGB fields
+ int rgbY = y + fieldHeight + spacing;
+ if (mouseY >= rgbY && mouseY < rgbY + fieldHeight) {
+ int fieldWidth = (width - spacing * 2 - (includeAlpha ? spacing : 0)) / (includeAlpha ? 4 : 3);
+
+ if (mouseX >= x && mouseX < x + fieldWidth) {
+ focusedInput = 2;
+ cursorPosition = rInputText.length();
+ } else if (mouseX >= x + fieldWidth + spacing && mouseX < x + fieldWidth * 2 + spacing) {
+ focusedInput = 3;
+ cursorPosition = gInputText.length();
+ } else if (mouseX >= x + (fieldWidth + spacing) * 2 && mouseX < x + fieldWidth * 3 + spacing * 2) {
+ focusedInput = 4;
+ cursorPosition = bInputText.length();
+ } else if (includeAlpha && mouseX >= x + (fieldWidth + spacing) * 3) {
+ focusedInput = 5;
+ cursorPosition = aInputText.length();
+ }
+ resetCursorBlink();
+ }
+ }
+
+ private void resetCursorBlink() {
+ cursorVisible = true;
+ lastCursorBlink = System.currentTimeMillis();
+ }
+
+ @Override
+ public void onRelease(double mouseX, double mouseY, int button) {
+ if (button == 0) {
+ if (isDraggingSV || isDraggingHue || isDraggingAlpha) {
+ onColorChanged();
+ }
+ isDraggingSV = false;
+ isDraggingHue = false;
+ isDraggingAlpha = false;
+ }
+ super.onRelease(mouseX, mouseY, button);
+ }
+
+ /**
+ * Handles mouse drag for color selection.
+ */
+ public void onMouseDragged(double mouseX, double mouseY, int button) {
+ if (button != 0 || !expanded) return;
+
+ Rectangle layout = getLayout();
+ int popupX = layout.x;
+ int popupY = layout.y + layout.height + 2;
+ int padding = 4;
+ int innerX = popupX + padding;
+ int innerY = popupY + padding;
+ int innerWidth = popupWidth - padding * 2;
+ int svSize = innerWidth - HUE_BAR_WIDTH - 4;
+
+ if (isDraggingSV) {
+ updateSVFromMouse(mouseX, mouseY, innerX, innerY, svSize);
+ } else if (isDraggingHue) {
+ updateHueFromMouse(mouseY, innerY, svSize);
+ } else if (isDraggingAlpha) {
+ updateAlphaFromMouse(mouseX, innerX, innerWidth);
+ }
+ }
+
+ private void updateSVFromMouse(double mouseX, double mouseY, int x, int y, int size) {
+ saturation = clamp((float) (mouseX - x) / size, 0f, 1f);
+ brightness = 1f - clamp((float) (mouseY - y) / size, 0f, 1f);
+ updateRGBFromHSV();
+ }
+
+ private void updateHueFromMouse(double mouseY, int y, int height) {
+ hue = clamp((float) (mouseY - y) / height, 0f, 1f);
+ updateRGBFromHSV();
+ }
+
+ private void updateAlphaFromMouse(double mouseX, int x, int width) {
+ alpha = clamp((int) ((mouseX - x) / width * 255), 0, 255);
+ aInputText = String.valueOf(alpha);
+ }
+
+ private static float clamp(float value, float min, float max) {
+ return Math.max(min, Math.min(max, value));
+ }
+
+ @Override
+ public boolean onKeyPressed(int keyCode, int scanCode, int modifiers) {
+ if (!expanded) return false;
+
+ // Handle Escape to close
+ if (keyCode == GLFW.GLFW_KEY_ESCAPE) {
+ expanded = false;
+ focusedInput = 0;
+ applyInputTexts();
+ return true;
+ }
+
+ // Handle Tab to cycle inputs
+ if (keyCode == GLFW.GLFW_KEY_TAB) {
+ applyInputTexts();
+ int maxInput = includeAlpha ? 5 : 4;
+ if ((modifiers & GLFW.GLFW_MOD_SHIFT) != 0) {
+ focusedInput = focusedInput <= 1 ? maxInput : focusedInput - 1;
+ } else {
+ focusedInput = focusedInput >= maxInput ? 1 : focusedInput + 1;
+ }
+ cursorPosition = getCurrentInputText().length();
+ resetCursorBlink();
+ return true;
+ }
+
+ // Handle Enter to apply and close
+ if (keyCode == GLFW.GLFW_KEY_ENTER) {
+ applyInputTexts();
+ expanded = false;
+ focusedInput = 0;
+ return true;
+ }
+
+ // Handle text input for focused field
+ if (focusedInput > 0) {
+ return handleTextInput(keyCode, modifiers);
+ }
+
+ return false;
+ }
+
+ private boolean handleTextInput(int keyCode, int modifiers) {
+ String text = getCurrentInputText();
+
+ switch (keyCode) {
+ case GLFW.GLFW_KEY_BACKSPACE:
+ if (cursorPosition > 0) {
+ text = text.substring(0, cursorPosition - 1) + text.substring(cursorPosition);
+ cursorPosition--;
+ setCurrentInputText(text);
+ resetCursorBlink();
+ }
+ return true;
+
+ case GLFW.GLFW_KEY_DELETE:
+ if (cursorPosition < text.length()) {
+ text = text.substring(0, cursorPosition) + text.substring(cursorPosition + 1);
+ setCurrentInputText(text);
+ resetCursorBlink();
+ }
+ return true;
+
+ case GLFW.GLFW_KEY_LEFT:
+ if (cursorPosition > 0) {
+ cursorPosition--;
+ resetCursorBlink();
+ }
+ return true;
+
+ case GLFW.GLFW_KEY_RIGHT:
+ if (cursorPosition < text.length()) {
+ cursorPosition++;
+ resetCursorBlink();
+ }
+ return true;
+
+ case GLFW.GLFW_KEY_HOME:
+ cursorPosition = 0;
+ resetCursorBlink();
+ return true;
+
+ case GLFW.GLFW_KEY_END:
+ cursorPosition = text.length();
+ resetCursorBlink();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onCharTyped(char chr, int modifiers) {
+ if (!expanded || focusedInput == 0) return false;
+
+ String text = getCurrentInputText();
+ int maxLength = focusedInput == 1 ? (includeAlpha ? 8 : 6) : 3;
+
+ // Validate character based on input type
+ boolean valid;
+ if (focusedInput == 1) {
+ // Hex input - allow 0-9, A-F
+ valid = (chr >= '0' && chr <= '9') || (chr >= 'A' && chr <= 'F') || (chr >= 'a' && chr <= 'f');
+ } else {
+ // RGB/A input - allow 0-9 only
+ valid = chr >= '0' && chr <= '9';
+ }
+
+ if (valid && text.length() < maxLength) {
+ char toInsert = focusedInput == 1 ? Character.toUpperCase(chr) : chr;
+ text = text.substring(0, cursorPosition) + toInsert + text.substring(cursorPosition);
+ cursorPosition++;
+ setCurrentInputText(text);
+ resetCursorBlink();
+
+ // Auto-apply for immediate feedback
+ applyInputTexts();
+ return true;
+ }
+
+ return false;
+ }
+
+ private String getCurrentInputText() {
+ return switch (focusedInput) {
+ case 1 -> hexInputText;
+ case 2 -> rInputText;
+ case 3 -> gInputText;
+ case 4 -> bInputText;
+ case 5 -> aInputText;
+ default -> "";
+ };
+ }
+
+ private void setCurrentInputText(String text) {
+ switch (focusedInput) {
+ case 1 -> hexInputText = text;
+ case 2 -> rInputText = text;
+ case 3 -> gInputText = text;
+ case 4 -> bInputText = text;
+ case 5 -> aInputText = text;
+ }
+ }
+
+ private void applyInputTexts() {
+ try {
+ // Apply hex if it's complete
+ if (hexInputText.length() == 6 || (includeAlpha && hexInputText.length() == 8)) {
+ setColorHex(hexInputText);
+ return;
+ }
+
+ // Apply RGB values
+ int r = rInputText.isEmpty() ? 0 : clamp(Integer.parseInt(rInputText), 0, 255);
+ int g = gInputText.isEmpty() ? 0 : clamp(Integer.parseInt(gInputText), 0, 255);
+ int b = bInputText.isEmpty() ? 0 : clamp(Integer.parseInt(bInputText), 0, 255);
+ int a = aInputText.isEmpty() ? 255 : clamp(Integer.parseInt(aInputText), 0, 255);
+
+ setColorInternal(r, g, b, includeAlpha ? a : alpha);
+ } catch (NumberFormatException e) {
+ // Invalid input, restore from current color
+ updateInputTexts();
+ }
+ }
+
+ @Override
+ public boolean canFocus() {
+ return true;
+ }
+
+ @Override
+ public void onFocusChanged(boolean focused) {
+ this.focused = focused;
+ if (!focused && expanded) {
+ // Don't close on focus loss - let click-outside handle it
+ }
+ }
+
+ /**
+ * Called when the color changes.
+ */
+ protected void onColorChanged() {
+ if (script != null && script.onColorChanged() != null && !script.onColorChanged().isnil()) {
+ Varargs args = LuaValue.varargsOf(new LuaValue[]{
+ LuaBinder.bind(new LuaElement(this)),
+ LuaValue.valueOf(red),
+ LuaValue.valueOf(green),
+ LuaValue.valueOf(blue),
+ LuaValue.valueOf(alpha)
+ });
+
+ try {
+ script.onColorChanged().invoke(args);
+ } catch (Exception e) {
+ AmbleKit.LOGGER.error("Error invoking onColorChanged script for AmbleColorPicker {}:", id(), e);
+ }
+ }
+ }
+
+ /**
+ * Checks if a point is within the expanded popup area.
+ */
+ public boolean isInExpandedArea(double mouseX, double mouseY) {
+ if (!expanded) return false;
+
+ Rectangle layout = getLayout();
+ int popupX = layout.x;
+ int popupY = layout.y + layout.height + 2;
+ int actualPopupHeight = popupHeight + (includeAlpha ? ALPHA_BAR_HEIGHT + 4 : 0);
+
+ return mouseX >= popupX && mouseX <= popupX + popupWidth &&
+ mouseY >= popupY && mouseY <= popupY + actualPopupHeight;
+ }
+
+ // ===== Builder =====
+
+ public static Builder colorPickerBuilder() {
+ return new Builder();
+ }
+
+ public static class Builder extends AbstractBuilder {
+
+ @Override
+ protected AmbleColorPicker create() {
+ return new AmbleColorPicker();
+ }
+
+ @Override
+ protected Builder self() {
+ return this;
+ }
+
+ public Builder color(int r, int g, int b, int a) {
+ container.setColorInternal(r, g, b, a);
+ return this;
+ }
+
+ public Builder color(int r, int g, int b) {
+ container.setColorInternal(r, g, b, 255);
+ return this;
+ }
+
+ public Builder colorHex(String hex) {
+ container.setColorHex(hex);
+ return this;
+ }
+
+ public Builder includeAlpha(boolean includeAlpha) {
+ container.setIncludeAlpha(includeAlpha);
+ return this;
+ }
+
+ public Builder borderCollapsed(Color color) {
+ container.setBorderCollapsed(color);
+ return this;
+ }
+
+ public Builder borderCollapsedHover(Color color) {
+ container.setBorderCollapsedHover(color);
+ return this;
+ }
+
+ public Builder borderExpanded(Color color) {
+ container.setBorderExpanded(color);
+ return this;
+ }
+
+ public Builder backgroundExpanded(Color color) {
+ container.setBackgroundExpanded(color);
+ return this;
+ }
+
+ public Builder popupWidth(int width) {
+ container.setPopupWidth(width);
+ return this;
+ }
+
+ public Builder popupHeight(int height) {
+ container.setPopupHeight(height);
+ return this;
+ }
+
+ public Builder script(LuaScript script) {
+ container.setScript(script);
+ return this;
+ }
+ }
+
+ // ===== Parser =====
+
+ /**
+ * Parser for AmbleColorPicker elements.
+ *
+ * This parser handles JSON objects that have the "color_picker" property set to true.
+ *
+ * Supported JSON properties:
+ *
+ * - {@code color_picker} - Boolean, must be true to create a color picker
+ * - {@code initial_color} - Color array [r,g,b] or [r,g,b,a]
+ * - {@code include_alpha} - Boolean, whether to show alpha slider (default: false)
+ * - {@code border_collapsed} - Color array for collapsed border
+ * - {@code border_collapsed_hover} - Color array for hovered collapsed border
+ * - {@code border_expanded} - Color array for expanded popup border
+ * - {@code background_expanded} - Color array for expanded popup background
+ * - {@code popup_width} - Integer width of popup in pixels
+ * - {@code popup_height} - Integer height of popup in pixels (excluding alpha bar)
+ * - {@code script} - Script ID for event handling
+ *
+ */
+ public static class Parser implements AmbleElementParser {
+
+ @Override
+ public @Nullable AmbleContainer parse(JsonObject json, @Nullable Identifier resourceId, AmbleContainer base) {
+ if (!json.has("color_picker") || !json.get("color_picker").getAsBoolean()) {
+ return null;
+ }
+
+ AmbleColorPicker picker = AmbleColorPicker.colorPickerBuilder().build();
+ picker.copyFrom(base);
+
+ // Parse initial color
+ if (json.has("initial_color")) {
+ JsonArray colorArray = json.get("initial_color").getAsJsonArray();
+ int r = colorArray.get(0).getAsInt();
+ int g = colorArray.get(1).getAsInt();
+ int b = colorArray.get(2).getAsInt();
+ int a = colorArray.size() > 3 ? colorArray.get(3).getAsInt() : 255;
+ picker.setColorInternal(r, g, b, a);
+ }
+
+ // Parse include_alpha
+ if (json.has("include_alpha")) {
+ picker.setIncludeAlpha(json.get("include_alpha").getAsBoolean());
+ }
+
+ // Parse border colors
+ if (json.has("border_collapsed")) {
+ picker.setBorderCollapsed(parseColor(json.get("border_collapsed").getAsJsonArray()));
+ }
+ if (json.has("border_collapsed_hover")) {
+ picker.setBorderCollapsedHover(parseColor(json.get("border_collapsed_hover").getAsJsonArray()));
+ }
+ if (json.has("border_expanded")) {
+ picker.setBorderExpanded(parseColor(json.get("border_expanded").getAsJsonArray()));
+ }
+ if (json.has("background_expanded")) {
+ picker.setBackgroundExpanded(parseColor(json.get("background_expanded").getAsJsonArray()));
+ }
+
+ // Parse popup dimensions
+ if (json.has("popup_width")) {
+ picker.setPopupWidth(json.get("popup_width").getAsInt());
+ }
+ if (json.has("popup_height")) {
+ picker.setPopupHeight(json.get("popup_height").getAsInt());
+ }
+
+ // Parse script
+ if (json.has("script")) {
+ Identifier scriptId = new Identifier(json.get("script").getAsString())
+ .withPrefixedPath("script/")
+ .withSuffixedPath(".lua");
+ LuaScript script = ScriptManager.getInstance().load(
+ scriptId,
+ MinecraftClient.getInstance().getResourceManager()
+ );
+ picker.setScript(script);
+ }
+
+ return picker;
+ }
+
+ private Color parseColor(JsonArray colorArray) {
+ int r = colorArray.get(0).getAsInt();
+ int g = colorArray.get(1).getAsInt();
+ int b = colorArray.get(2).getAsInt();
+ int a = colorArray.size() > 3 ? colorArray.get(3).getAsInt() : 255;
+ return new Color(r, g, b, a);
+ }
+
+ @Override
+ public int priority() {
+ return 90;
+ }
+ }
+}
+
diff --git a/src/main/java/dev/amble/lib/client/gui/AmbleContainer.java b/src/main/java/dev/amble/lib/client/gui/AmbleContainer.java
index 443fa85..933c40f 100644
--- a/src/main/java/dev/amble/lib/client/gui/AmbleContainer.java
+++ b/src/main/java/dev/amble/lib/client/gui/AmbleContainer.java
@@ -218,6 +218,14 @@ public boolean mouseDragged(double mouseX, double mouseY, int button, double del
textInput.onMouseDragged(mouseX, mouseY, button);
return true;
}
+ if (focusedElement instanceof AmbleSlider slider) {
+ slider.onMouseDragged(mouseX, mouseY, button);
+ return true;
+ }
+ if (focusedElement instanceof AmbleColorPicker colorPicker) {
+ colorPicker.onMouseDragged(mouseX, mouseY, button);
+ return true;
+ }
return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
}
diff --git a/src/main/java/dev/amble/lib/client/gui/AmbleSlider.java b/src/main/java/dev/amble/lib/client/gui/AmbleSlider.java
new file mode 100644
index 0000000..31cc086
--- /dev/null
+++ b/src/main/java/dev/amble/lib/client/gui/AmbleSlider.java
@@ -0,0 +1,484 @@
+package dev.amble.lib.client.gui;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import dev.amble.lib.AmbleKit;
+import dev.amble.lib.client.gui.lua.LuaElement;
+import dev.amble.lib.client.gui.registry.AmbleElementParser;
+import dev.amble.lib.script.LuaScript;
+import dev.amble.lib.script.ScriptManager;
+import dev.amble.lib.script.lua.LuaBinder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
+import org.luaj.vm2.LuaValue;
+import org.luaj.vm2.Varargs;
+
+import java.awt.*;
+
+/**
+ * A slider element that allows users to select a value within a range.
+ *
+ * Features:
+ *
+ * - Horizontal slider with configurable min/max values
+ * - Customizable track and thumb colors
+ * - Mouse drag support for smooth value adjustment
+ * - Keyboard support (arrow keys) when focused
+ * - Script callback for value changes
+ *
+ */
+@Getter
+@NoArgsConstructor
+public class AmbleSlider extends AmbleContainer implements Focusable {
+
+ // Value properties
+ @Setter
+ private float min = 0.0f;
+ @Setter
+ private float max = 1.0f;
+ private float value = 0.0f;
+
+ // Focus state
+ @Setter
+ private boolean focused = false;
+
+ // Visual customization
+ @Setter
+ private Color trackColor = new Color(60, 60, 60);
+ @Setter
+ private Color trackFilledColor = new Color(80, 140, 200);
+ @Setter
+ private Color thumbColor = new Color(200, 200, 200);
+ @Setter
+ private Color thumbHoverColor = new Color(255, 255, 255);
+ @Setter
+ private Color borderColor = new Color(100, 100, 100);
+ @Setter
+ private Color focusedBorderColor = new Color(80, 160, 255);
+
+ @Setter
+ private int trackHeight = 4;
+ @Setter
+ private int thumbWidth = 8;
+ @Setter
+ private int thumbHeight = 16;
+
+ // Interaction state
+ private boolean isDragging = false;
+ private boolean isThumbHovered = false;
+
+ // Step for keyboard navigation (0 = continuous)
+ @Setter
+ private float step = 0.0f;
+
+ // Script support
+ @Setter
+ private @Nullable LuaScript script;
+
+ /**
+ * Sets the current value, clamping to min/max range.
+ */
+ public void setValue(float value) {
+ float oldValue = this.value;
+ this.value = Math.max(min, Math.min(max, value));
+ if (oldValue != this.value) {
+ onValueChanged();
+ }
+ }
+
+ /**
+ * Gets the value as a normalized 0-1 range.
+ */
+ public float getNormalizedValue() {
+ if (max == min) return 0;
+ return (value - min) / (max - min);
+ }
+
+ /**
+ * Sets the value from a normalized 0-1 range.
+ */
+ public void setNormalizedValue(float normalized) {
+ setValue(min + normalized * (max - min));
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ // Render background
+ getBackground().render(context, getLayout());
+
+ Rectangle layout = getLayout();
+ int padding = getPadding();
+
+ // Calculate track area
+ int trackX = layout.x + padding;
+ int trackY = layout.y + (layout.height - trackHeight) / 2;
+ int trackWidth = layout.width - padding * 2;
+
+ // Draw track background
+ context.fill(trackX, trackY, trackX + trackWidth, trackY + trackHeight, trackColor.getRGB());
+
+ // Draw filled portion of track
+ int filledWidth = (int) (trackWidth * getNormalizedValue());
+ context.fill(trackX, trackY, trackX + filledWidth, trackY + trackHeight, trackFilledColor.getRGB());
+
+ // Calculate thumb position
+ int thumbX = trackX + filledWidth - thumbWidth / 2;
+ int thumbY = layout.y + (layout.height - thumbHeight) / 2;
+
+ // Clamp thumb within track bounds
+ thumbX = Math.max(trackX - thumbWidth / 2, Math.min(trackX + trackWidth - thumbWidth / 2, thumbX));
+
+ // Check if thumb is hovered
+ isThumbHovered = mouseX >= thumbX && mouseX <= thumbX + thumbWidth &&
+ mouseY >= thumbY && mouseY <= thumbY + thumbHeight;
+
+ // Draw thumb
+ Color currentThumbColor = (isThumbHovered || isDragging) ? thumbHoverColor : thumbColor;
+ context.fill(thumbX, thumbY, thumbX + thumbWidth, thumbY + thumbHeight, currentThumbColor.getRGB());
+
+ // Draw border
+ Color border = focused ? focusedBorderColor : borderColor;
+ drawBorder(context, layout, border);
+
+ // Render children
+ for (AmbleElement child : getChildren()) {
+ if (child.isVisible()) {
+ child.render(context, mouseX, mouseY, delta);
+ }
+ }
+ }
+
+ private void drawBorder(DrawContext context, Rectangle layout, Color color) {
+ int x = layout.x;
+ int y = layout.y;
+ int w = layout.width;
+ int h = layout.height;
+ int c = color.getRGB();
+
+ // Top
+ context.fill(x, y, x + w, y + 1, c);
+ // Bottom
+ context.fill(x, y + h - 1, x + w, y + h, c);
+ // Left
+ context.fill(x, y, x + 1, y + h, c);
+ // Right
+ context.fill(x + w - 1, y, x + w, y + h, c);
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY, int button) {
+ if (button == 0 && isHovered(mouseX, mouseY)) {
+ isDragging = true;
+ updateValueFromMouse(mouseX);
+ }
+ super.onClick(mouseX, mouseY, button);
+ }
+
+ @Override
+ public void onRelease(double mouseX, double mouseY, int button) {
+ if (button == 0) {
+ isDragging = false;
+ }
+ super.onRelease(mouseX, mouseY, button);
+ }
+
+ /**
+ * Handles mouse drag for value adjustment.
+ */
+ public void onMouseDragged(double mouseX, double mouseY, int button) {
+ if (isDragging && button == 0) {
+ updateValueFromMouse(mouseX);
+ }
+ }
+
+ private void updateValueFromMouse(double mouseX) {
+ Rectangle layout = getLayout();
+ int padding = getPadding();
+ int trackX = layout.x + padding;
+ int trackWidth = layout.width - padding * 2;
+
+ // Calculate normalized value from mouse position
+ float normalized = (float) (mouseX - trackX) / trackWidth;
+ normalized = Math.max(0, Math.min(1, normalized));
+
+ // Apply step if set
+ if (step > 0) {
+ float range = max - min;
+ float steps = range / step;
+ normalized = Math.round(normalized * steps) / steps;
+ }
+
+ setNormalizedValue(normalized);
+ }
+
+ @Override
+ public boolean onKeyPressed(int keyCode, int scanCode, int modifiers) {
+ if (!focused) return false;
+
+ float increment = step > 0 ? step : (max - min) / 100f;
+
+ // Handle Ctrl for larger steps
+ if ((modifiers & org.lwjgl.glfw.GLFW.GLFW_MOD_CONTROL) != 0) {
+ increment *= 10;
+ }
+
+ switch (keyCode) {
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_LEFT:
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_DOWN:
+ setValue(value - increment);
+ return true;
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_RIGHT:
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_UP:
+ setValue(value + increment);
+ return true;
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_HOME:
+ setValue(min);
+ return true;
+ case org.lwjgl.glfw.GLFW.GLFW_KEY_END:
+ setValue(max);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onCharTyped(char chr, int modifiers) {
+ // Slider doesn't handle character input
+ return false;
+ }
+
+ @Override
+ public boolean canFocus() {
+ return true;
+ }
+
+ @Override
+ public void onFocusChanged(boolean focused) {
+ this.focused = focused;
+ }
+
+ /**
+ * Called when the value changes.
+ */
+ protected void onValueChanged() {
+ if (script != null && script.onValueChanged() != null && !script.onValueChanged().isnil()) {
+ Varargs args = LuaValue.varargsOf(new LuaValue[]{
+ LuaBinder.bind(new LuaElement(this)),
+ LuaValue.valueOf(value)
+ });
+
+ try {
+ script.onValueChanged().invoke(args);
+ } catch (Exception e) {
+ AmbleKit.LOGGER.error("Error invoking onValueChanged script for AmbleSlider {}:", id(), e);
+ }
+ }
+ }
+
+ // ===== Builder =====
+
+ public static Builder sliderBuilder() {
+ return new Builder();
+ }
+
+ public static class Builder extends AbstractBuilder {
+
+ @Override
+ protected AmbleSlider create() {
+ return new AmbleSlider();
+ }
+
+ @Override
+ protected Builder self() {
+ return this;
+ }
+
+ public Builder min(float min) {
+ container.setMin(min);
+ return this;
+ }
+
+ public Builder max(float max) {
+ container.setMax(max);
+ return this;
+ }
+
+ public Builder value(float value) {
+ container.setValue(value);
+ return this;
+ }
+
+ public Builder step(float step) {
+ container.setStep(step);
+ return this;
+ }
+
+ public Builder trackColor(Color color) {
+ container.setTrackColor(color);
+ return this;
+ }
+
+ public Builder trackFilledColor(Color color) {
+ container.setTrackFilledColor(color);
+ return this;
+ }
+
+ public Builder thumbColor(Color color) {
+ container.setThumbColor(color);
+ return this;
+ }
+
+ public Builder thumbHoverColor(Color color) {
+ container.setThumbHoverColor(color);
+ return this;
+ }
+
+ public Builder borderColor(Color color) {
+ container.setBorderColor(color);
+ return this;
+ }
+
+ public Builder focusedBorderColor(Color color) {
+ container.setFocusedBorderColor(color);
+ return this;
+ }
+
+ public Builder trackHeight(int height) {
+ container.setTrackHeight(height);
+ return this;
+ }
+
+ public Builder thumbWidth(int width) {
+ container.setThumbWidth(width);
+ return this;
+ }
+
+ public Builder thumbHeight(int height) {
+ container.setThumbHeight(height);
+ return this;
+ }
+
+ public Builder script(LuaScript script) {
+ container.setScript(script);
+ return this;
+ }
+ }
+
+ // ===== Parser =====
+
+ /**
+ * Parser for AmbleSlider elements.
+ *
+ * This parser handles JSON objects that have the "slider" property set to true.
+ *
+ * Supported JSON properties:
+ *
+ * - {@code slider} - Boolean, must be true to create a slider
+ * - {@code min} - Float minimum value (default: 0)
+ * - {@code max} - Float maximum value (default: 1)
+ * - {@code value} - Float initial value (default: 0)
+ * - {@code step} - Float step increment for snapping (default: 0 = continuous)
+ * - {@code track_color} - Color array [r,g,b] or [r,g,b,a]
+ * - {@code track_filled_color} - Color array for filled portion
+ * - {@code thumb_color} - Color array for thumb
+ * - {@code thumb_hover_color} - Color array for hovered thumb
+ * - {@code border_color} - Color array for border
+ * - {@code focused_border_color} - Color array for focused border
+ * - {@code track_height} - Integer height of track in pixels
+ * - {@code thumb_width} - Integer width of thumb in pixels
+ * - {@code thumb_height} - Integer height of thumb in pixels
+ * - {@code script} - Script ID for event handling
+ *
+ */
+ public static class Parser implements AmbleElementParser {
+
+ @Override
+ public @Nullable AmbleContainer parse(JsonObject json, @Nullable Identifier resourceId, AmbleContainer base) {
+ if (!json.has("slider") || !json.get("slider").getAsBoolean()) {
+ return null;
+ }
+
+ AmbleSlider slider = AmbleSlider.sliderBuilder().build();
+ slider.copyFrom(base);
+
+ // Parse min/max/value
+ if (json.has("min")) {
+ slider.setMin(json.get("min").getAsFloat());
+ }
+ if (json.has("max")) {
+ slider.setMax(json.get("max").getAsFloat());
+ }
+ if (json.has("value")) {
+ slider.setValue(json.get("value").getAsFloat());
+ }
+ if (json.has("step")) {
+ slider.setStep(json.get("step").getAsFloat());
+ }
+
+ // Parse colors
+ if (json.has("track_color")) {
+ slider.setTrackColor(parseColor(json.get("track_color").getAsJsonArray()));
+ }
+ if (json.has("track_filled_color")) {
+ slider.setTrackFilledColor(parseColor(json.get("track_filled_color").getAsJsonArray()));
+ }
+ if (json.has("thumb_color")) {
+ slider.setThumbColor(parseColor(json.get("thumb_color").getAsJsonArray()));
+ }
+ if (json.has("thumb_hover_color")) {
+ slider.setThumbHoverColor(parseColor(json.get("thumb_hover_color").getAsJsonArray()));
+ }
+ if (json.has("border_color")) {
+ slider.setBorderColor(parseColor(json.get("border_color").getAsJsonArray()));
+ }
+ if (json.has("focused_border_color")) {
+ slider.setFocusedBorderColor(parseColor(json.get("focused_border_color").getAsJsonArray()));
+ }
+
+ // Parse dimensions
+ if (json.has("track_height")) {
+ slider.setTrackHeight(json.get("track_height").getAsInt());
+ }
+ if (json.has("thumb_width")) {
+ slider.setThumbWidth(json.get("thumb_width").getAsInt());
+ }
+ if (json.has("thumb_height")) {
+ slider.setThumbHeight(json.get("thumb_height").getAsInt());
+ }
+
+ // Parse script
+ if (json.has("script")) {
+ Identifier scriptId = new Identifier(json.get("script").getAsString())
+ .withPrefixedPath("script/")
+ .withSuffixedPath(".lua");
+ LuaScript script = ScriptManager.getInstance().load(
+ scriptId,
+ MinecraftClient.getInstance().getResourceManager()
+ );
+ slider.setScript(script);
+ }
+
+ return slider;
+ }
+
+ private Color parseColor(JsonArray colorArray) {
+ int r = colorArray.get(0).getAsInt();
+ int g = colorArray.get(1).getAsInt();
+ int b = colorArray.get(2).getAsInt();
+ int a = colorArray.size() > 3 ? colorArray.get(3).getAsInt() : 255;
+ return new Color(r, g, b, a);
+ }
+
+ @Override
+ public int priority() {
+ return 85;
+ }
+ }
+}
+
diff --git a/src/main/java/dev/amble/lib/client/gui/Draggable.java b/src/main/java/dev/amble/lib/client/gui/Draggable.java
new file mode 100644
index 0000000..59bb417
--- /dev/null
+++ b/src/main/java/dev/amble/lib/client/gui/Draggable.java
@@ -0,0 +1,24 @@
+package dev.amble.lib.client.gui;
+
+/**
+ * Interface for GUI elements that support mouse drag interactions.
+ *
+ * Implementing this interface allows elements to receive mouse drag events
+ * from the screen's drag handler. This is used by elements like sliders,
+ * color pickers, and text inputs for selection dragging.
+ */
+public interface Draggable {
+
+ /**
+ * Called when the mouse is dragged while this element is the drag target.
+ *
+ * This method is called continuously while the mouse button is held down
+ * and the mouse is being moved.
+ *
+ * @param mouseX the current mouse X position
+ * @param mouseY the current mouse Y position
+ * @param button the mouse button being held (0 = left, 1 = right, 2 = middle)
+ */
+ void onMouseDragged(double mouseX, double mouseY, int button);
+}
+
diff --git a/src/main/java/dev/amble/lib/client/gui/lua/LuaElement.java b/src/main/java/dev/amble/lib/client/gui/lua/LuaElement.java
index 918a24f..1ae61cb 100644
--- a/src/main/java/dev/amble/lib/client/gui/lua/LuaElement.java
+++ b/src/main/java/dev/amble/lib/client/gui/lua/LuaElement.java
@@ -568,6 +568,229 @@ public void setEntityScale(float scale) {
}
}
+ // ===== AmbleSlider methods =====
+
+ /**
+ * Gets the current slider value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @return the current value, or 0 if not a slider
+ */
+ @LuaExpose
+ public float getValue() {
+ if (element instanceof AmbleSlider slider) {
+ return slider.getValue();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the slider value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @param value the value to set (will be clamped to min/max)
+ */
+ @LuaExpose
+ public void setValue(float value) {
+ if (element instanceof AmbleSlider slider) {
+ slider.setValue(value);
+ }
+ }
+
+ /**
+ * Gets the slider minimum value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @return the minimum value, or 0 if not a slider
+ */
+ @LuaExpose
+ public float getMin() {
+ if (element instanceof AmbleSlider slider) {
+ return slider.getMin();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the slider minimum value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @param min the minimum value
+ */
+ @LuaExpose
+ public void setMin(float min) {
+ if (element instanceof AmbleSlider slider) {
+ slider.setMin(min);
+ }
+ }
+
+ /**
+ * Gets the slider maximum value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @return the maximum value, or 0 if not a slider
+ */
+ @LuaExpose
+ public float getMax() {
+ if (element instanceof AmbleSlider slider) {
+ return slider.getMax();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the slider maximum value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @param max the maximum value
+ */
+ @LuaExpose
+ public void setMax(float max) {
+ if (element instanceof AmbleSlider slider) {
+ slider.setMax(max);
+ }
+ }
+
+ /**
+ * Gets the slider step value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @return the step value, or 0 if not a slider
+ */
+ @LuaExpose
+ public float getStep() {
+ if (element instanceof AmbleSlider slider) {
+ return slider.getStep();
+ }
+ return 0;
+ }
+
+ /**
+ * Sets the slider step value.
+ * Only works if the underlying element is an AmbleSlider.
+ *
+ * @param step the step value (0 = continuous)
+ */
+ @LuaExpose
+ public void setStep(float step) {
+ if (element instanceof AmbleSlider slider) {
+ slider.setStep(step);
+ }
+ }
+
+ // ===== AmbleColorPicker methods =====
+
+ /**
+ * Gets the current color as RGB values.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @return array of [r, g, b, a] values (0-255), or null if not a color picker
+ */
+ @LuaExpose
+ public int[] getColorRGBA() {
+ if (element instanceof AmbleColorPicker picker) {
+ return new int[] { picker.getRed(), picker.getGreen(), picker.getBlue(), picker.getAlpha() };
+ }
+ return null;
+ }
+
+ /**
+ * Sets the color from RGBA values.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @param r red component (0-255)
+ * @param g green component (0-255)
+ * @param b blue component (0-255)
+ * @param a alpha component (0-255)
+ */
+ @LuaExpose
+ public void setColorRGBA(int r, int g, int b, int a) {
+ if (element instanceof AmbleColorPicker picker) {
+ picker.setColor(r, g, b, a);
+ }
+ }
+
+ /**
+ * Gets the current color as a hex string.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @return hex string (RRGGBB or RRGGBBAA), or null if not a color picker
+ */
+ @LuaExpose
+ public String getColorHex() {
+ if (element instanceof AmbleColorPicker picker) {
+ return picker.getColorHex();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the color from a hex string.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @param hex hex string (with or without #, 6 or 8 characters)
+ */
+ @LuaExpose
+ public void setColorHex(String hex) {
+ if (element instanceof AmbleColorPicker picker) {
+ picker.setColorHex(hex);
+ }
+ }
+
+ /**
+ * Checks if the color picker is expanded.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @return true if expanded, false otherwise
+ */
+ @LuaExpose
+ public boolean isPickerExpanded() {
+ if (element instanceof AmbleColorPicker picker) {
+ return picker.isExpanded();
+ }
+ return false;
+ }
+
+ /**
+ * Sets the expanded state of the color picker.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @param expanded true to expand, false to collapse
+ */
+ @LuaExpose
+ public void setPickerExpanded(boolean expanded) {
+ if (element instanceof AmbleColorPicker picker) {
+ picker.setExpanded(expanded);
+ }
+ }
+
+ /**
+ * Checks if the color picker includes alpha support.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @return true if alpha is included, false otherwise
+ */
+ @LuaExpose
+ public boolean isIncludeAlpha() {
+ if (element instanceof AmbleColorPicker picker) {
+ return picker.isIncludeAlpha();
+ }
+ return false;
+ }
+
+ /**
+ * Sets whether the color picker includes alpha support.
+ * Only works if the underlying element is an AmbleColorPicker.
+ *
+ * @param includeAlpha true to include alpha slider
+ */
+ @LuaExpose
+ public void setIncludeAlpha(boolean includeAlpha) {
+ if (element instanceof AmbleColorPicker picker) {
+ picker.setIncludeAlpha(includeAlpha);
+ }
+ }
+
/**
* Returns the underlying AmbleElement wrapped by this LuaElement.
* This method is for internal use only and should not be called from Lua scripts.
diff --git a/src/main/java/dev/amble/lib/client/gui/registry/AmbleGuiRegistry.java b/src/main/java/dev/amble/lib/client/gui/registry/AmbleGuiRegistry.java
index 187cafe..3736723 100644
--- a/src/main/java/dev/amble/lib/client/gui/registry/AmbleGuiRegistry.java
+++ b/src/main/java/dev/amble/lib/client/gui/registry/AmbleGuiRegistry.java
@@ -37,6 +37,8 @@ private AmbleGuiRegistry() {
registerParser(new AmbleText.Parser());
registerParser(new AmbleEntityDisplay.Parser());
registerParser(new AmbleTextInput.Parser());
+ registerParser(new AmbleSlider.Parser());
+ registerParser(new AmbleColorPicker.Parser());
}
/**
diff --git a/src/main/java/dev/amble/lib/script/LuaScript.java b/src/main/java/dev/amble/lib/script/LuaScript.java
index 9471d24..80b9a6c 100644
--- a/src/main/java/dev/amble/lib/script/LuaScript.java
+++ b/src/main/java/dev/amble/lib/script/LuaScript.java
@@ -72,4 +72,20 @@ public LuaValue onRelease() {
public LuaValue onHover() {
return getCallback("onHover");
}
+
+ // ===== Value change callbacks =====
+
+ /**
+ * Called when a slider's value changes.
+ */
+ public LuaValue onValueChanged() {
+ return getCallback("onValueChanged");
+ }
+
+ /**
+ * Called when a color picker's color changes.
+ */
+ public LuaValue onColorChanged() {
+ return getCallback("onColorChanged");
+ }
}
diff --git a/src/test/resources/assets/litmus/gui/skin_changer.json b/src/test/resources/assets/litmus/gui/skin_changer.json
index 7e5fc3d..236f767 100644
--- a/src/test/resources/assets/litmus/gui/skin_changer.json
+++ b/src/test/resources/assets/litmus/gui/skin_changer.json
@@ -97,7 +97,7 @@
"placeholder": "Enter username...",
"max_length": 16,
"layout": [
- 154,
+ 130,
20
],
"background": [
@@ -128,6 +128,18 @@
120
]
},
+ {
+ "id": "litmus:name_color_picker",
+ "color_picker": true,
+ "initial_color": [255, 255, 255],
+ "include_alpha": false,
+ "layout": [20, 20],
+ "background": [20, 20, 30, 255],
+ "border_collapsed": [80, 80, 100],
+ "border_collapsed_hover": [100, 140, 220],
+ "background_expanded": [30, 30, 45, 245],
+ "border_expanded": [60, 60, 80]
+ },
{
"id": "litmus:set_skin_btn",
"script": "litmus:skin_changer",
@@ -157,56 +169,56 @@
]
},
{
- "id": "litmus:slim_toggle_btn",
- "script": "litmus:skin_changer_slim",
+ "id": "litmus:set_username_btn",
+ "script": "litmus:username_changer",
"hover_background": [
+ 100,
80,
- 80,
- 120
+ 70
],
"press_background": [
+ 70,
50,
- 50,
- 80
+ 40
],
"layout": [
154,
20
],
"background": [
+ 80,
60,
- 60,
- 90
+ 50
],
- "text": "§fModel: Classic",
+ "text": "§fSet Username",
"text_alignment": [
"centre",
"centre"
]
},
{
- "id": "litmus:set_username_btn",
- "script": "litmus:username_changer",
+ "id": "litmus:slim_toggle_btn",
+ "script": "litmus:skin_changer_slim",
"hover_background": [
- 100,
80,
- 70
+ 80,
+ 120
],
"press_background": [
- 70,
50,
- 40
+ 50,
+ 80
],
"layout": [
154,
20
],
"background": [
- 80,
60,
- 50
+ 60,
+ 90
],
- "text": "§fSet Username",
+ "text": "§fModel: Classic",
"text_alignment": [
"centre",
"centre"
diff --git a/src/test/resources/assets/litmus/script/username_changer.lua b/src/test/resources/assets/litmus/script/username_changer.lua
index 4c1786b..3cc4ba0 100644
--- a/src/test/resources/assets/litmus/script/username_changer.lua
+++ b/src/test/resources/assets/litmus/script/username_changer.lua
@@ -1,5 +1,5 @@
-- Username Changer Script
--- Handles the "Set Username" button functionality
+-- Handles the "Set Username" button functionality with color support
local GuiUtils = require("litmus:gui_utils")
@@ -9,10 +9,22 @@ function onClick(self, mouseX, mouseY, button)
local username, input, statusText = GuiUtils.validateInput(self, "litmus:username_input", "litmus:status_text")
if not username then return end
- -- Set the username using the Lua API
- if mc:setUsername(mc:username(), username) then
+ -- Get color from color picker
+ local root = getRoot(self)
+ local colorPicker = findById(root, "litmus:name_color_picker")
+
+ local colorHex = "FFFFFF"
+ if colorPicker then
+ colorHex = colorPicker:getColorHex()
+ end
+
+ -- Build JSON text component with the selected color
+ local jsonText = '{"text":"' .. username .. '","color":"#' .. colorHex .. '"}'
+
+ -- Set the username using the Lua API with JSON text
+ if mc:setUsernameJson(mc:username(), jsonText) then
-- Success feedback
- GuiUtils.onSuccess(mc, input, statusText, "Username set to: " .. username)
+ GuiUtils.onSuccess(mc, input, statusText, "Username set to: §#" .. colorHex .. username)
else
if statusText then
statusText:setText("§cFailed to set username!")