Skip to content

Instantly share code, notes, and snippets.

@Ensamisten
Created April 10, 2026 20:10
Show Gist options
  • Select an option

  • Save Ensamisten/10fee1f235036566c81587ab829c94c3 to your computer and use it in GitHub Desktop.

Select an option

Save Ensamisten/10fee1f235036566c81587ab829c94c3 to your computer and use it in GitHub Desktop.
package io.github.ensamisten.client.gui;
import io.github.ensamisten.client.config.ButtonsConfig;
import io.github.ensamisten.client.module.Extension;
import io.github.ensamisten.client.module.ModuleManager;
import io.github.ensamisten.client.module.screen.FriendGuardConfigScreen;
import io.github.ensamisten.client.module.screen.KillAuraConfigScreen;
import io.github.ensamisten.client.module.screen.WhomStruckMeLastScreen;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.ChatFormatting;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Options;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.options.OptionsScreen;
import net.minecraft.client.input.CharacterEvent;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
import org.lwjgl.glfw.GLFW;
import java.util.*;
@Environment(EnvType.CLIENT)
public class AllyshipOptionsScreen extends OptionsScreen {
// ── Hardcoded category order ──────────────────────────────────
private static final List<String> CATEGORIES = List.of(
"Combat", "Movement", "Render", "Exploit", "World", "Player"
);
private static final int BUTTON_WIDTH = 80;
private static final int BUTTON_HEIGHT = 20;
private static final int DROPDOWN_WIDTH = BUTTON_WIDTH;
private static final int BORDER_PADDING = 4;
private static final int MAX_VISIBLE_ITEMS = 8;
private static final int SEARCH_BAR_W = 160;
private static final int SEARCH_BAR_H = 16;
private static final int SEARCH_BAR_Y = 10;
private final Map<String, DropdownContainer> dropdowns = new LinkedHashMap<>();
private final Map<String, Button> categoryButtons = new LinkedHashMap<>();
// ── Search state ─────────────────────────���────────────────────
private EditBox searchBar;
private String searchQuery = "";
// Search results — populated when query is non-empty
private final List<SearchResultItem> searchResults = new ArrayList<>();
// Drag state
private Button draggedButton = null;
private double initialClickX, initialClickY;
private double dragOffsetX, dragOffsetY;
private boolean isDragging = false;
private final double dragThreshold = 5.0;
private final Minecraft client;
public AllyshipOptionsScreen(Screen parent, Options options) {
super(parent, options, true);
this.client = Minecraft.getInstance();
for (String category : CATEGORIES) {
List<DropdownItem> items = buildItemsForCategory(category);
dropdowns.put(category.toLowerCase(), new DropdownContainer(category, items));
}
}
// ── Build items ───────────────────────────────────────────────
private List<DropdownItem> buildItemsForCategory(String category) {
List<DropdownItem> items = new ArrayList<>();
for (Extension module : ModuleManager.getAll()) {
if (module.getCategory().equalsIgnoreCase(category)) {
items.add(new DropdownItem(module.getName(), null, null));
}
}
return items;
}
// ── init ──────────────────────────────────────────────────────
// ── init ──────────────────────────────────────────────────────
@Override
protected void init() {
int sbX = width / 2 - SEARCH_BAR_W / 2;
searchBar = new EditBox(font, sbX, SEARCH_BAR_Y,
SEARCH_BAR_W, SEARCH_BAR_H,
Component.literal("Search modules..."));
searchBar.setMaxLength(32);
searchBar.setHint(Component.literal("Search modules..."));
searchBar.setResponder(this::onSearchTyped);
addRenderableWidget(searchBar);
int buttonY = 60;
int buttonSpacing = 10;
int currentX = 20;
for (String categoryKey : dropdowns.keySet()) {
DropdownContainer container = dropdowns.get(categoryKey);
final int defaultX = currentX;
final int defaultY = 60;
// Restore saved position or fall back to default
int[] saved = ButtonsConfig.getInstance().getPosition(categoryKey);
final int x = saved != null ? saved[0] : defaultX;
final int y = saved != null ? saved[1] : defaultY;
Button button = Button.builder(
Component.literal(container.getDisplayName()),
btn -> {}
).bounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT).build();
addRenderableOnly(button);
categoryButtons.put(categoryKey, button);
currentX += BUTTON_WIDTH + buttonSpacing;
}
}
// ── Search logic ──────────────────────────────────────────────
private void onSearchTyped(String query) {
searchQuery = query.trim().toLowerCase();
searchResults.clear();
if (searchQuery.isEmpty()) return;
// Close all dropdowns while searching
for (DropdownContainer dc : dropdowns.values()) {
if (dc.isVisible()) dc.toggle();
}
// Find all modules whose name contains the query
for (Extension module : ModuleManager.getAll()) {
if (module.getName().toLowerCase().contains(searchQuery)) {
searchResults.add(new SearchResultItem(module.getName()));
}
}
}
// ── Mouse input ───────────────────────────────────────────────
@Override
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
double mouseX = event.x();
double mouseY = event.y();
if (!searchQuery.isEmpty()) {
if (searchBar.isMouseOver(mouseX, mouseY)) {
return super.mouseClicked(event, doubleClick);
}
for (SearchResultItem item : searchResults) {
if (item.mouseClicked(event, doubleClick)) return true;
}
return true;
}
// Drag tracking — category buttons are renderableOnly so we handle them here
for (Map.Entry<String, Button> entry : categoryButtons.entrySet()) {
Button categoryButton = entry.getValue();
if (categoryButton.isMouseOver(mouseX, mouseY)) {
draggedButton = categoryButton;
initialClickX = mouseX;
initialClickY = mouseY;
dragOffsetX = mouseX - categoryButton.getX();
dragOffsetY = mouseY - categoryButton.getY();
return true; // consume — mouseReleased will toggle
}
}
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.mouseClicked(event, doubleClick)) return true;
}
return super.mouseClicked(event, doubleClick);
}
@Override
public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) {
double mouseX = event.x();
double mouseY = event.y();
if (draggedButton != null) {
double distance = Math.hypot(mouseX - initialClickX, mouseY - initialClickY);
if (!isDragging && distance > dragThreshold) isDragging = true;
if (isDragging) {
int newX = (int)(mouseX - dragOffsetX);
int newY = (int)(mouseY - dragOffsetY);
String categoryKey = draggedButton.getMessage().getString().toLowerCase();
DropdownContainer dropdown = dropdowns.get(categoryKey);
if (dropdown != null && dropdown.isVisible()) {
int dropdownHeight = Math.min(dropdown.getDesiredHeight(), MAX_VISIBLE_ITEMS * BUTTON_HEIGHT);
int maxY = this.height - BUTTON_HEIGHT - dropdownHeight - BORDER_PADDING;
newX = Math.max(0, Math.min(this.width - BUTTON_WIDTH, newX));
newY = Math.max(0, Math.min(maxY, newY));
} else {
newX = Math.max(-BUTTON_WIDTH, Math.min(this.width, newX));
newY = Math.max(-BUTTON_HEIGHT, Math.min(this.height, newY));
}
draggedButton.setX(newX);
draggedButton.setY(newY);
if (dropdown != null && dropdown.isVisible()) dropdown.setPosition(newX, newY);
return true;
}
}
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.mouseDragged(mouseX, mouseY, event.button(), deltaX, deltaY)) return true;
}
return super.mouseDragged(event, deltaX, deltaY);
}
@Override
public boolean mouseReleased(MouseButtonEvent event) {
if (draggedButton != null) {
if (!isDragging && event.button() == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
if (draggedButton.isMouseOver(event.x(), event.y())) {
String categoryKey = draggedButton.getMessage().getString().toLowerCase();
toggleDropdown(categoryKey, draggedButton);
}
}
if (isDragging) {
// Save the new position after a completed drag
String categoryKey = draggedButton.getMessage().getString().toLowerCase();
ButtonsConfig.getInstance().setPosition(categoryKey,
draggedButton.getX(), draggedButton.getY());
ButtonsConfig.getInstance().save();
}
draggedButton = null;
isDragging = false;
return true;
}
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.mouseReleased(event.x(), event.y(), event.button())) return true;
}
return super.mouseReleased(event);
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double hAmount, double vAmount) {
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.mouseScrolled(mouseX, mouseY, hAmount, vAmount)) return true;
}
return super.mouseScrolled(mouseX, mouseY, hAmount, vAmount);
}
// ── Keyboard input ────────────────────────────────────────────
@Override
public boolean keyPressed(KeyEvent event) {
// ESC clears search first, then closes screen
if (event.key() == GLFW.GLFW_KEY_ESCAPE && !searchQuery.isEmpty()) {
searchBar.setValue("");
searchQuery = "";
searchResults.clear();
return true;
}
if (searchBar.isFocused() && searchBar.keyPressed(event)) return true;
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.keyPressed(event)) return true;
}
return super.keyPressed(event);
}
@Override
public boolean charTyped(CharacterEvent event) {
if (searchBar.isFocused() && searchBar.charTyped(event)) return true;
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible() && dropdown.charTyped(event)) return true;
}
return super.charTyped(event);
}
// ── Rendering ─────────────────────────────────────────────────
@Override
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
// transparent
}
@Override
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
super.extractRenderState(graphics, mouseX, mouseY, a); // renders searchBar automatically
if (!searchQuery.isEmpty()) {
int resultX = width / 2 - SEARCH_BAR_W / 2;
int resultY = SEARCH_BAR_Y + SEARCH_BAR_H + 2;
int panelW = SEARCH_BAR_W;
if (searchResults.isEmpty()) {
graphics.fill(resultX, resultY, resultX + panelW, resultY + BUTTON_HEIGHT, 0xAA000000);
graphics.text(font, "§7No results found.",
resultX + 4, resultY + (BUTTON_HEIGHT - 8) / 2, 0xFFAAAAAA);
} else {
int y = resultY;
for (SearchResultItem item : searchResults) {
item.setPosition(resultX, y, panelW);
item.extractRenderState(graphics, mouseX, mouseY, a);
y += BUTTON_HEIGHT;
}
}
} else {
updateDropdownPositions();
for (DropdownContainer dropdown : dropdowns.values()) {
if (dropdown.isVisible()) dropdown.extractRenderState(graphics, mouseX, mouseY, a);
}
}
// NOTE: do NOT call searchBar.extractRenderState manually — super already did it
}
// ── Helpers ───────────────────────────────────────────────────
private void toggleDropdown(String categoryKey, Button button) {
DropdownContainer dropdown = dropdowns.get(categoryKey);
if (dropdown != null) dropdown.toggle();
}
private void updateDropdownPositions() {
for (Map.Entry<String, DropdownContainer> entry : dropdowns.entrySet()) {
DropdownContainer dropdown = entry.getValue();
if (dropdown.isVisible()) {
Button button = categoryButtons.get(entry.getKey());
if (button != null) dropdown.setPosition(button.getX(), button.getY());
}
}
}
private Screen getConfigScreen(String moduleName) {
return switch (moduleName) {
case "FriendGuard" -> new FriendGuardConfigScreen(AllyshipOptionsScreen.this);
case "KillAura" -> new KillAuraConfigScreen(AllyshipOptionsScreen.this);
case "WhomStruckMeLast" -> new WhomStruckMeLastScreen(AllyshipOptionsScreen.this);
default -> null;
};
}
// -----------------------------------------------------------------------
// SearchResultItem — a single row in the search results panel
// -----------------------------------------------------------------------
private class SearchResultItem {
private final String name;
private int posX, posY, rowW;
SearchResultItem(String name) { this.name = name; }
void setPosition(int x, int y, int w) { posX = x; posY = y; rowW = w; }
boolean isMouseOver(double mx, double my) {
return mx >= posX && mx <= posX + rowW
&& my >= posY && my <= posY + BUTTON_HEIGHT;
}
boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (!isMouseOver(event.x(), event.y())) return false;
if (event.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT) {
Screen cfg = getConfigScreen(name);
if (cfg != null) { client.setScreen(cfg); return true; }
}
// Left-click → toggle module
Extension module = ModuleManager.getByName(name);
if (module != null) { module.toggle(); return true; }
return false;
}
void extractRenderState(GuiGraphicsExtractor graphics,
int mouseX, int mouseY, float a) {
boolean hovered = isMouseOver(mouseX, mouseY);
graphics.fill(posX, posY, posX + rowW, posY + BUTTON_HEIGHT,
hovered ? 0xCC333333 : 0xAA000000);
Extension module = ModuleManager.getByName(name);
int nameColor = (module != null && module.isEnabled()) ? 0xFF55FF55 : 0xFFAAAAAA;
graphics.text(font, name, posX + 4, posY + (BUTTON_HEIGHT - 8) / 2, nameColor);
// Category tag on the right
if (module != null) {
String cat = "§7[" + module.getCategory() + "]";
graphics.text(font, cat,
posX + rowW - font.width(module.getCategory() + "[]") - 4,
posY + (BUTTON_HEIGHT - 8) / 2, 0xFFAAAAAA);
}
}
}
// -----------------------------------------------------------------------
// DropdownContainer (unchanged from your original)
// -----------------------------------------------------------------------
private class DropdownContainer {
private final String displayName;
private final List<DropdownItem> items;
private boolean visible = false;
private int posX, posY;
private int scrollOffset = 0;
DropdownContainer(String displayName, List<DropdownItem> items) {
this.displayName = displayName;
this.items = items;
}
public String getDisplayName() { return displayName; }
public boolean isVisible() { return visible; }
public int getHeight() {
int maxContainer = MAX_VISIBLE_ITEMS * BUTTON_HEIGHT;
int screenHeight = Minecraft.getInstance().getWindow().getGuiScaledHeight();
int maxBelowButton = screenHeight - posY - BORDER_PADDING;
int maxHeight = Math.max(Math.min(maxContainer, maxBelowButton), BUTTON_HEIGHT);
return Math.min(getDesiredHeight(), maxHeight);
}
// Add to DropdownContainer
public int getWidth() {
int maxW = DROPDOWN_WIDTH;
for (DropdownItem item : items) {
int textW = Minecraft.getInstance().font.width(item.getName()) + 8; // 4px padding each side
if (textW > maxW) maxW = textW;
}
return maxW;
}
public int getDesiredHeight() { return calculateTotalHeight(); }
public void toggle() { visible = !visible; updateItemPositions(); }
public void setPosition(int x, int y) {
this.posX = x;
this.posY = y + BUTTON_HEIGHT;
updateItemPositions();
}
private void updateItemPositions() {
int currentY = posY - scrollOffset;
for (DropdownItem item : items) {
boolean itemVis = currentY + item.getTotalHeight() > posY
&& currentY < posY + getHeight();
item.setVisible(itemVis);
item.setPosition(posX, currentY);
currentY += item.getTotalHeight();
if (item.hasSubOptions() && item.isSubOptionsVisible()) {
for (DropdownItem sub : item.getSubOptions()) {
boolean subVis = currentY + sub.getTotalHeight() > posY
&& currentY < posY + getHeight();
sub.setVisible(subVis);
sub.setPosition(posX, currentY);
currentY += sub.getTotalHeight();
}
}
}
}
public boolean mouseScrolled(double mouseX, double mouseY, double hAmount, double vAmount) {
if (!visible) return false;
if (mouseX < posX || mouseX > posX + getWidth()
|| mouseY < posY || mouseY > posY + getHeight()) return false;
scrollOffset = Math.clamp(
scrollOffset - (int)(vAmount * BUTTON_HEIGHT),
0, Math.max(0, calculateTotalHeight() - getHeight()));
updateItemPositions();
return true;
}
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (!visible) return false;
if (event.x() < posX || event.x() > posX + getWidth()
|| event.y() < posY || event.y() > posY + getHeight()) return false;
for (DropdownItem item : items) {
if (item.isVisible() && item.mouseClicked(event, doubleClick)) return true;
}
return false;
}
public boolean mouseReleased(double mouseX, double mouseY, int button) {
if (!visible) return false;
for (DropdownItem item : items) { if (item.isVisible()) return true; }
return false;
}
public boolean mouseDragged(double mouseX, double mouseY, int button, double dX, double dY) {
if (!visible) return false;
for (DropdownItem item : items) { if (item.isVisible()) return true; }
return false;
}
public boolean keyPressed(KeyEvent event) {
for (DropdownItem item : items) {
if (item.isVisible() && item.keyPressed(event)) return true;
}
return false;
}
public boolean charTyped(CharacterEvent event) {
for (DropdownItem item : items) {
if (item.isVisible() && item.charTyped(event)) return true;
}
return false;
}
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
if (!visible) return;
int w = getWidth();
// Single unified background — no per-item fill stacking on top
graphics.fill(posX, posY, posX + w, posY + getHeight(), 0xCC000000);
for (DropdownItem item : items) {
if (item.isVisible()) item.extractRenderState(graphics, mouseX, mouseY, a);
}
}
private int calculateTotalHeight() {
int h = 0;
for (DropdownItem item : items) {
h += item.getTotalHeight();
if (item.hasSubOptions() && item.isSubOptionsVisible()) {
for (DropdownItem sub : item.getSubOptions()) h += sub.getTotalHeight();
}
}
return h;
}
}
// -----------------------------------------------------------------------
// DropdownItem (unchanged from your original)
// -----------------------------------------------------------------------
private class DropdownItem {
private final String name;
List<DropdownItem> subOptions;
private boolean subOptionsVisible = false;
private boolean visible = false;
private int posX, posY;
private EditBox textField;
private boolean textFieldVisible = false;
DropdownItem(String name, List<DropdownItem> subOptions, DropdownItem parent) {
this.name = name;
this.subOptions = subOptions;
if (name.equals("CopyChat") || name.equals("sierra") || name.equals("Title")) {
this.textField = new EditBox(
font, posX, posY + BUTTON_HEIGHT,
DROPDOWN_WIDTH, BUTTON_HEIGHT,
Component.literal("Enter Target"));
this.textField.setVisible(false);
addWidget(this.textField);
}
}
public boolean hasSubOptions() { return subOptions != null && !subOptions.isEmpty(); }
public List<DropdownItem> getSubOptions() { return subOptions; }
public void toggleSubOptions() { if (hasSubOptions()) subOptionsVisible = !subOptionsVisible; }
public boolean isSubOptionsVisible() { return subOptionsVisible; }
public boolean isVisible() { return visible; }
public String getName() { return name; }
public void setPosition(int x, int y) {
posX = x; posY = y;
if (textField != null) { textField.setX(x); textField.setY(y + BUTTON_HEIGHT); }
}
public void setVisible(boolean visible) {
this.visible = visible;
if (textField != null) textField.setVisible(textFieldVisible && visible);
}
public int getTotalHeight() {
int h = BUTTON_HEIGHT;
if (textFieldVisible && textField != null) h += BUTTON_HEIGHT;
return h;
}
public boolean isMouseOver(double mouseX, double mouseY) {
return mouseX >= posX && mouseX <= posX + DROPDOWN_WIDTH
&& mouseY >= posY && mouseY <= posY + getTotalHeight();
}
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
if (textFieldVisible && textField != null && textField.isMouseOver(event.x(), event.y()))
return textField.mouseClicked(event, doubleClick);
if (hasSubOptions() && isSubOptionsVisible()) {
for (DropdownItem sub : subOptions) {
if (sub.mouseClicked(event, doubleClick)) return true;
}
}
if (isMouseOver(event.x(), event.y())) {
Extension module = ModuleManager.getByName(name);
if (event.button() == GLFW.GLFW_MOUSE_BUTTON_RIGHT) {
Screen configScreen = getConfigScreen(name);
if (configScreen != null) { client.setScreen(configScreen); return true; }
}
if (module != null) { module.toggle(); return true; }
toggleSubOptions();
return true;
}
return false;
}
public boolean keyPressed(KeyEvent event) {
if (textFieldVisible && textField != null && textField.isFocused()) {
int key = event.key();
if (key == GLFW.GLFW_KEY_ENTER || key == GLFW.GLFW_KEY_KP_ENTER) {
onSaveTextField(); return true;
}
return textField.keyPressed(event);
}
return false;
}
public boolean charTyped(CharacterEvent event) {
if (textFieldVisible && textField != null && textField.isFocused())
return textField.charTyped(event);
return false;
}
private void onSaveTextField() {
String input = textField.getValue().trim();
if (!input.isEmpty()) {
switch (name) {
case "sierra" -> client.player.sendSystemMessage(
Component.literal(ChatFormatting.GREEN + "Now staring at entity type: " + input));
}
} else {
client.player.sendSystemMessage(
Component.literal(ChatFormatting.RED + "Please enter a valid target."));
}
textFieldVisible = false;
textField.setVisible(false);
textField.setFocused(false);
setFocused(null);
for (DropdownContainer d : dropdowns.values()) d.updateItemPositions();
}
private Screen getConfigScreen(String moduleName) {
return switch (moduleName) {
case "FriendGuard" -> new FriendGuardConfigScreen(AllyshipOptionsScreen.this);
case "KillAura" -> new KillAuraConfigScreen(AllyshipOptionsScreen.this);
case "WhomStruckMeLast" -> new WhomStruckMeLastScreen(AllyshipOptionsScreen.this);
default -> null;
};
}
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float a) {
int w = Minecraft.getInstance().font.width(name) + 8;
w = Math.max(w, DROPDOWN_WIDTH);
Extension module = ModuleManager.getByName(name);
int color = (module != null && module.isEnabled()) ? 0xFF55FF55 : 0xFFAAAAAA;
// ── Removed: graphics.fill() here — container panel is the background ──
graphics.text(font, name, posX + 4, posY + (BUTTON_HEIGHT - 8) / 2, color);
if (textFieldVisible && textField != null)
textField.extractRenderState(graphics, mouseX, mouseY, a);
if (hasSubOptions() && isSubOptionsVisible() && subOptions != null)
for (DropdownItem sub : subOptions)
if (sub.isVisible()) sub.extractRenderState(graphics, mouseX, mouseY, a);
}
}
@Override
public void onClose() {
// Persist all current button positions before leaving
for (Map.Entry<String, Button> entry : categoryButtons.entrySet()) {
ButtonsConfig.getInstance().setPosition(entry.getKey(),
entry.getValue().getX(),
entry.getValue().getY());
}
ButtonsConfig.getInstance().save();
super.onClose();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment