diff --git a/src/client/java/me/madeq/client/gui/DropdownPanel.java b/src/client/java/me/madeq/client/gui/DropdownPanel.java index 22093e6..10ebcff 100644 --- a/src/client/java/me/madeq/client/gui/DropdownPanel.java +++ b/src/client/java/me/madeq/client/gui/DropdownPanel.java @@ -1,6 +1,7 @@ package me.madeq.client.gui; import java.awt.Color; +import java.util.Collections; import java.util.List; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; @@ -32,6 +33,10 @@ public String getTitle() { return title; } + public List getAllEntries() { + return Collections.unmodifiableList(entries); + } + public void render(GuiGraphics graphics, Font font, int mouseX, int mouseY) { updateAnimation(); @@ -135,4 +140,4 @@ private void updateAnimation() { private boolean isInside(double mouseX, double mouseY, int x, int y, int width, int height) { return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; } -} +} \ No newline at end of file diff --git a/src/client/java/me/madeq/client/gui/SearchBar.java b/src/client/java/me/madeq/client/gui/SearchBar.java new file mode 100644 index 0000000..2afa9c4 --- /dev/null +++ b/src/client/java/me/madeq/client/gui/SearchBar.java @@ -0,0 +1,251 @@ +package me.madeq.client.gui; + +import java.awt.Color; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import org.lwjgl.glfw.GLFW; + +public class SearchBar { + + private static final int ICON_WIDTH = 14; + private static final int PADDING_X = 5; + private static final int MAX_LENGTH = 64; + + private static final int COL_BG = new Color(10, 13, 19, 236).getRGB(); + private static final int COL_BG_SHADOW = new Color( 3, 5, 9, 150).getRGB(); + private static final int COL_BORDER_IDLE = new Color(48, 58, 74 ).getRGB(); + private static final int COL_BORDER_ACTIVE = new Color(74, 163, 255 ).getRGB(); + private static final int COL_TEXT = new Color(220, 226, 235 ).getRGB(); + private static final int COL_PLACEHOLDER = new Color(110, 122, 140 ).getRGB(); + private static final int COL_ICON = new Color( 90, 110, 140 ).getRGB(); + private static final int COL_CLEAR_IDLE = new Color(110, 122, 140 ).getRGB(); + private static final int COL_CLEAR_HOVER = new Color(220, 100, 100 ).getRGB(); + + private final int x; + private final int y; + private final int width; + private final int height; + + private String value = ""; + private int cursor = 0; + private boolean allSelected = false; + private boolean focused = true; + + public SearchBar(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public String getValue() { + return value.trim(); + } + + public void clear() { + value = ""; + cursor = 0; + allSelected = false; + } + + public void render(GuiGraphics graphics, Font font, int mouseX, int mouseY) { + boolean hovered = isInside(mouseX, mouseY); + + graphics.fill(x - 1, y - 1, x + width + 1, y + height + 1, COL_BG_SHADOW); + graphics.fill(x, y, x + width, y + height, COL_BG); + int borderCol = (focused || hovered) ? COL_BORDER_ACTIVE : COL_BORDER_IDLE; + graphics.fill(x, y + height - 1, x + width, y + height, borderCol); + + graphics.drawString(font, "\uD83D\uDD0D", x + PADDING_X, y + (height - 8) / 2, COL_ICON, false); + + int textX = x + PADDING_X + ICON_WIDTH; + int clearButtonWidth = value.isEmpty() ? 0 : font.width("×") + PADDING_X; + int textAreaWidth = width - PADDING_X - ICON_WIDTH - clearButtonWidth - PADDING_X; + + if (value.isEmpty() && !focused) { + graphics.drawString(font, "Search modules…", textX, y + (height - 8) / 2, COL_PLACEHOLDER, false); + } else { + String displayed = getDisplayedValue(font, textAreaWidth); + graphics.drawString(font, displayed, textX, y + (height - 8) / 2, COL_TEXT, false); + } + + if (!value.isEmpty()) { + boolean clearHovered = isClearButtonHovered(mouseX, mouseY); + int clearX = x + width - font.width("×") - PADDING_X; + int clearY = y + (height - 8) / 2; + graphics.drawString(font, "×", clearX, clearY, + clearHovered ? COL_CLEAR_HOVER : COL_CLEAR_IDLE, false); + } + } + + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button != 0) { + return false; + } + + if (!value.isEmpty() && isClearButtonHovered(mouseX, mouseY)) { + clear(); + focused = true; + return true; + } + + if (isInside(mouseX, mouseY)) { + focused = true; + cursor = value.length(); + allSelected = false; + return true; + } + + focused = false; + return false; + } + + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!focused) { + return false; + } + + boolean ctrl = (modifiers & GLFW.GLFW_MOD_CONTROL) != 0 + || (modifiers & GLFW.GLFW_MOD_SUPER) != 0; + + if (ctrl) { + return handleControlShortcut(keyCode); + } + + switch (keyCode) { + case GLFW.GLFW_KEY_BACKSPACE -> { deletePrevious(); return true; } + case GLFW.GLFW_KEY_DELETE -> { deleteNext(); return true; } + case GLFW.GLFW_KEY_LEFT -> { moveCursor(-1); return true; } + case GLFW.GLFW_KEY_RIGHT -> { moveCursor(+1); return true; } + case GLFW.GLFW_KEY_HOME -> { cursor = 0; allSelected = false; return true; } + case GLFW.GLFW_KEY_END -> { cursor = value.length(); allSelected = false; return true; } + } + + return false; + } + + public boolean charTyped(char codePoint) { + if (Character.isISOControl(codePoint)) { + return false; + } + + focused = true; + + if (value.length() >= MAX_LENGTH && !allSelected) { + return false; + } + + insert(Character.toString(codePoint)); + return true; + } + + private boolean handleControlShortcut(int keyCode) { + if (keyCode == GLFW.GLFW_KEY_A) { + allSelected = true; + cursor = value.length(); + return true; + } + if (keyCode == GLFW.GLFW_KEY_C) { + if (!value.isEmpty()) { + Minecraft.getInstance().keyboardHandler.setClipboard(value); + } + return true; + } + if (keyCode == GLFW.GLFW_KEY_V) { + insert(Minecraft.getInstance().keyboardHandler.getClipboard()); + return true; + } + if (keyCode == GLFW.GLFW_KEY_X) { + if (!value.isEmpty()) { + Minecraft.getInstance().keyboardHandler.setClipboard(value); + clear(); + } + return true; + } + return false; + } + + private void insert(String text) { + if (text == null || text.isEmpty()) { + return; + } + + if (allSelected) { + value = ""; + cursor = 0; + allSelected = false; + } + + StringBuilder clean = new StringBuilder(); + for (char c : text.toCharArray()) { + if (!Character.isISOControl(c)) { + clean.append(c); + } + } + + int available = MAX_LENGTH - value.length(); + if (available <= 0) { + return; + } + + String chunk = clean.substring(0, Math.min(available, clean.length())); + value = value.substring(0, cursor) + chunk + value.substring(cursor); + cursor += chunk.length(); + } + + private void deletePrevious() { + if (allSelected) { clear(); return; } + if (cursor <= 0 || value.isEmpty()) return; + value = value.substring(0, cursor - 1) + value.substring(cursor); + cursor--; + } + + private void deleteNext() { + if (allSelected) { clear(); return; } + if (cursor >= value.length()) return; + value = value.substring(0, cursor) + value.substring(cursor + 1); + } + + private void moveCursor(int delta) { + cursor = Math.max(0, Math.min(value.length(), cursor + delta)); + allSelected = false; + } + + private String getDisplayedValue(Font font, int maxPixels) { + String full = value; + int start = 0; + + if (font.width(full.substring(start, cursor)) > maxPixels - 8) { + while (start < cursor && font.width(full.substring(start, cursor)) > maxPixels - 8) { + start++; + } + } + + String visible = full.substring(start); + + if (focused) { + boolean showCursor = (System.currentTimeMillis() / 480L) % 2L == 0L; + if (showCursor || allSelected) { + int localCursor = Math.max(0, Math.min(cursor - start, visible.length())); + if (allSelected) { + visible = "[" + visible + "]"; + } else { + visible = visible.substring(0, localCursor) + "_" + visible.substring(localCursor); + } + } + } + + return visible; + } + + private boolean isInside(double mx, double my) { + return mx >= x && mx <= x + width && my >= y && my <= y + height; + } + + private boolean isClearButtonHovered(double mx, double my) { + Font f = Minecraft.getInstance().font; + int clearW = f.width("×") + PADDING_X; + return mx >= x + width - clearW && mx <= x + width && my >= y && my <= y + height; + } +} \ No newline at end of file diff --git a/src/client/java/me/madeq/client/gui/impl/ClickGuiScreen.java b/src/client/java/me/madeq/client/gui/impl/ClickGuiScreen.java index b3ead49..2f4a43b 100644 --- a/src/client/java/me/madeq/client/gui/impl/ClickGuiScreen.java +++ b/src/client/java/me/madeq/client/gui/impl/ClickGuiScreen.java @@ -1,8 +1,10 @@ package me.madeq.client.gui.impl; +import java.awt.Color; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import me.madeq.client.LiteClient; import me.madeq.client.command.Command; @@ -10,31 +12,136 @@ import me.madeq.client.gui.DropdownPanel; import me.madeq.client.gui.GuiModuleEntry; import me.madeq.client.gui.GuiModuleExecutor; +import me.madeq.client.gui.SearchBar; import me.madeq.client.module.ModuleType; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; public class ClickGuiScreen extends Screen { + private static final int SEARCH_BAR_HEIGHT = 20; + private static final int SEARCH_BAR_PADDING = 6; + private static final int SEARCH_BAR_WIDTH = 160; + private final List panels = new ArrayList<>(); + private final List searchResults = new ArrayList<>(); + private SearchBar searchBar; public ClickGuiScreen() { super(Component.literal("FreeClient ClickGUI")); - buildPanels(); } + @Override + protected void init() { + super.init(); + int barX = width - SEARCH_BAR_WIDTH - SEARCH_BAR_PADDING; + int barY = SEARCH_BAR_PADDING; + searchBar = new SearchBar(barX, barY, SEARCH_BAR_WIDTH, SEARCH_BAR_HEIGHT); + } + @Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { super.renderTransparentBackground(graphics); + String query = searchBar.getValue(); + + if (query.isEmpty()) { + for (DropdownPanel panel : panels) { + panel.render(graphics, font, mouseX, mouseY); + } + } else { + rebuildSearchResults(query); + renderSearchResults(graphics, mouseX, mouseY); + } + + searchBar.render(graphics, font, mouseX, mouseY); + } + + private void rebuildSearchResults(String query) { + searchResults.clear(); + String lower = query.toLowerCase(Locale.ROOT); + for (DropdownPanel panel : panels) { - panel.render(graphics, font, mouseX, mouseY); + for (GuiModuleEntry entry : panel.getAllEntries()) { + if (entry.name().toLowerCase(Locale.ROOT).contains(lower)) { + searchResults.add(entry); + } + } + } + } + + private void renderSearchResults(GuiGraphics graphics, int mouseX, int mouseY) { + if (searchResults.isEmpty()) { + String msg = "No results"; + int cx = width / 2 - font.width(msg) / 2; + graphics.drawString(font, msg, cx, height / 2, new Color(130, 140, 160).getRGB(), true); + return; + } + + int panelX = 16; + int panelY = 24; + int panelWidth = 160; + int headerHeight = 18; + int entryHeight = 16; + int totalHeight = headerHeight + searchResults.size() * entryHeight; + + graphics.fill(panelX - 1, panelY - 1, panelX + panelWidth + 1, panelY + totalHeight + 1, + new Color(5, 6, 8, 185).getRGB()); + graphics.fill(panelX, panelY, panelX + panelWidth, panelY + headerHeight, + new Color(25, 92, 210).getRGB()); + graphics.fill(panelX, panelY + headerHeight - 1, panelX + panelWidth, panelY + headerHeight, + new Color(102, 169, 255).getRGB()); + graphics.drawString(font, "Results (" + searchResults.size() + ")", + panelX + 6, panelY + 5, new Color(250, 252, 255).getRGB(), true); + + graphics.fill(panelX, panelY + headerHeight, panelX + panelWidth, panelY + totalHeight, + new Color(16, 18, 23, 218).getRGB()); + + for (int i = 0; i < searchResults.size(); i++) { + GuiModuleEntry entry = searchResults.get(i); + int entryY = panelY + headerHeight + i * entryHeight; + boolean hovered = mouseX >= panelX && mouseX <= panelX + panelWidth + && mouseY >= entryY && mouseY <= entryY + entryHeight; + + graphics.fill(panelX, entryY, panelX + panelWidth, entryY + entryHeight, + hovered ? new Color(30, 36, 48, 235).getRGB() + : new Color(20, 23, 30, 210).getRGB()); + + String badge = entry.moduleType().getDisplayName(); + int badgeX = panelX + panelWidth - font.width(badge) - 6; + graphics.drawString(font, badge, badgeX, entryY + 4, + new Color(74, 130, 200).getRGB(), true); + + String label = truncate(entry.name(), font, badgeX - (panelX + 8) - 4); + graphics.drawString(font, label, panelX + 8, entryY + 4, + new Color(220, 226, 235).getRGB(), true); } } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (searchBar.mouseClicked(mouseX, mouseY, button)) { + return true; + } + + String query = searchBar.getValue(); + + if (!query.isEmpty()) { + if (button == 0 || button == 1) { + GuiModuleEntry entry = getSearchResultAt(mouseX, mouseY); + if (entry != null) { + if (button == 1) { + openDetails(entry); + } else { + GuiModuleExecutor.execute(entry); + } + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + if (button == 1) { for (DropdownPanel panel : panels) { GuiModuleEntry entry = panel.getEntryAt(mouseX, mouseY); @@ -66,7 +173,7 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { - if (button == 0) { + if (button == 0 && searchBar.getValue().isEmpty()) { for (DropdownPanel panel : panels) { if (panel.mouseDragged(mouseX, mouseY)) { return true; @@ -88,23 +195,81 @@ public boolean mouseReleased(double mouseX, double mouseY, int button) { return super.mouseReleased(mouseX, mouseY, button); } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == org.lwjgl.glfw.GLFW.GLFW_KEY_ESCAPE && !searchBar.getValue().isEmpty()) { + searchBar.clear(); + return true; + } + + if (searchBar.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + if (searchBar.charTyped(codePoint)) { + return true; + } + + return super.charTyped(codePoint, modifiers); + } + @Override public boolean isPauseScreen() { return false; } + private GuiModuleEntry getSearchResultAt(double mouseX, double mouseY) { + int panelX = 16; + int panelY = 24; + int panelWidth = 160; + int headerHeight = 18; + int entryHeight = 16; + + for (int i = 0; i < searchResults.size(); i++) { + int entryY = panelY + headerHeight + i * entryHeight; + if (mouseX >= panelX && mouseX <= panelX + panelWidth + && mouseY >= entryY && mouseY <= entryY + entryHeight) { + return searchResults.get(i); + } + } + + return null; + } + + private String truncate(String text, net.minecraft.client.gui.Font f, int maxPixels) { + if (f.width(text) <= maxPixels) { + return text; + } + + String ellipsis = "…"; + int ellipsisWidth = f.width(ellipsis); + + while (!text.isEmpty() && f.width(text) + ellipsisWidth > maxPixels) { + text = text.substring(0, text.length() - 1); + } + + return text + ellipsis; + } + private void buildPanels() { Map> groupedEntries = new LinkedHashMap<>(); for (Command command : LiteClient.getCommandManager().getCommands()) { if (command.isVisibleInGui()) { - groupedEntries.computeIfAbsent(command.getModuleType(), ignored -> new ArrayList<>()).add(GuiModuleEntry.command(command)); + groupedEntries.computeIfAbsent(command.getModuleType(), ignored -> new ArrayList<>()) + .add(GuiModuleEntry.command(command)); } } for (Module module : LiteClient.getModuleManager().getModules()) { if (module.isVisibleInGui()) { - groupedEntries.computeIfAbsent(module.getModuleType(), ignored -> new ArrayList<>()).add(GuiModuleEntry.module(module)); + groupedEntries.computeIfAbsent(module.getModuleType(), ignored -> new ArrayList<>()) + .add(GuiModuleEntry.module(module)); } } @@ -129,4 +294,4 @@ private void openDetails(GuiModuleEntry entry) { minecraft.setScreen(ModuleDetailsScreen.forModules(entry.module())); } } -} +} \ No newline at end of file