Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2014 PaleoCrafter, Ordinastie * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.malisis.core.client.gui.component.interaction; import java.util.LinkedList; import java.util.List; import net.malisis.core.client.gui.GuiRenderer; import net.malisis.core.client.gui.MalisisGui; import net.malisis.core.client.gui.component.UIComponent; import net.malisis.core.client.gui.component.control.IScrollable; import net.malisis.core.client.gui.component.control.UIScrollBar.Type; import net.malisis.core.client.gui.component.control.UISlimScrollbar; import net.malisis.core.client.gui.element.GuiShape; import net.malisis.core.client.gui.element.SimpleGuiShape; import net.malisis.core.client.gui.element.XYResizableGuiShape; import net.malisis.core.client.gui.event.ComponentEvent; import net.malisis.core.client.gui.event.KeyboardEvent; import net.malisis.core.client.gui.event.MouseEvent; import net.malisis.core.client.gui.event.component.ContentUpdateEvent; import net.malisis.core.client.gui.icon.GuiIcon; import net.malisis.core.util.MouseButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.StatCollector; import org.apache.commons.lang3.StringUtils; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; import com.google.common.eventbus.Subscribe; /** * UITextField. * * @author Ordinastie */ public class UITextField extends UIComponent<UITextField> implements IScrollable { /** Current text of this {@link UITextField}. */ protected StringBuilder text = new StringBuilder(); /** Different lines if {@link #multiLine} is <code>true</code>. */ protected List<String> lines = new LinkedList<>(); /** Whether this {@link UITextField} handles multiline text. */ protected boolean multiLine = false; /** Current cursor position. */ protected CursorPosition cursorPosition; /** Current selection cursor position. */ protected CursorPosition selectionPosition; /** Whether currently selecting text. */ protected boolean selectingText = false; /** Number of character offset out of this {@link UITextField} when drawn. */ protected int charOffset = 0; /** Number of line offset out of this {@link UITextField} when drawn. Always 0 if {@link #multiLine} is false */ protected int lineOffset = 0; /** Space used between each line. */ protected int lineSpacing = 1; /** Cursor blink timer. */ protected long startTimer; /** Whether this {@link UITextField} should select the text when release left mouse button. */ private boolean selectAllOnRelease = false; /** Whether this {@link UITextField} should auto select the text when gaining focus. */ protected boolean autoSelectOnFocus = false; /** Color of the text for this {@link UITextField}. */ protected int textColor = 0xFFFFFF; /** Scrollbar of the textfield **/ protected UISlimScrollbar scrollBar; /** Font scale used to draw the text *. */ protected float fontScale = 1; /** Shape used to draw the cursor of this {@link UITextField}. */ protected GuiShape cursorShape; /** Shape used to draw the selection box. */ protected GuiShape selectShape; /** Icon used to draw this {@link UITextField}. */ protected GuiIcon iconTextfield; /** Icon used to draw this {@link UITextField} when disabled. */ protected GuiIcon iconTextfieldDisabled; /** * Instantiates a new {@link UITextField}. * * @param gui the gui * @param text the text * @param multiLine whether the textfield handles multiple lines */ public UITextField(MalisisGui gui, String text, boolean multiLine) { super(gui); this.multiLine = multiLine; cursorPosition = new CursorPosition(); selectionPosition = new CursorPosition(); if (text != null) this.setText(text); createShape(gui); if (multiLine) scrollBar = new UISlimScrollbar(gui, this, Type.VERTICAL); } /** * Instantiates a new single lined {@link UITextField}. * * @param gui the gui * @param text the text */ public UITextField(MalisisGui gui, String text) { this(gui, text, false); } /** * Instantiates a new empty {@link UITextField}. * * @param gui the gui * @param multiLine the multi line */ public UITextField(MalisisGui gui, boolean multiLine) { this(gui, null, multiLine); } /** * Creates the shapes used by this {@link UITextField}. * * @param gui the gui */ protected void createShape(MalisisGui gui) { shape = new XYResizableGuiShape(1); cursorShape = new SimpleGuiShape(); selectShape = new SimpleGuiShape(); iconTextfield = gui.getGuiTexture().getXYResizableIcon(200, 30, 9, 12, 1); iconTextfieldDisabled = gui.getGuiTexture().getXYResizableIcon(200, 42, 9, 12, 1); } // #region getters/setters /** * Gets the text of this {@link UITextField}. * * @return the text */ public String getText() { return text.toString(); } /** * Sets the text of this {@link UITextField} and place the cursor at the end. * * @param text the new text */ public void setText(String text) { if (!validateText(text)) return; this.text.setLength(0); this.text.append(text); buildLines(); selectingText = false; if (focused) cursorPosition.jumpToEnd(); // fireEvent(new TextChanged(this)); } /** * Sets the size of this {@link UITextField}.<br> * If {@link #multiLine} is <code>false</code>, <b>height</b> is forced to 12. * * @param width the width * @param height the height * @return the UI text field */ @Override public UITextField setSize(int width, int height) { return super.setSize(width, multiLine ? height : 12); } /** * Sets the focused. * * @param focused the new focused */ @Override public void setFocused(boolean focused) { if (isDisabled() || !isVisible()) return; if (!this.focused) selectAllOnRelease = true; super.setFocused(focused); } /** * Sets the font scale for this {@link UITextField}. * * @param fontScale the font scale * @return this {@link UITextField} */ public UITextField setFontScale(float fontScale) { this.fontScale = fontScale; buildLines(); return this; } /** * Gets the font scale used by this {@link UITextField}. * * @return the font scale */ public float getFontScale() { return fontScale; } /** * Gets the line spacing used when drawing. * * @return the lineSpacing */ public int getLineSpacing() { return lineSpacing; } /** * Sets the line offset. * * @param line the new line offset */ public void setLineOffset(int line) { this.lineOffset = line; } /** * Sets the line spacing for this {@link UITextField}. * * @param lineSpacing the lineSpacing to set * @return this {@link UITextField} */ public UITextField setLineSpacing(int lineSpacing) { this.lineSpacing = lineSpacing; return this; } /** * Gets the line height of this {@link UITextField}. * * @return the line height */ public int getLineHeight() { return (int) Math.ceil((GuiRenderer.FONT_HEIGHT + lineSpacing) * fontScale); } /** * Gets the current cursor position. * * @return the position of the cursor. */ public CursorPosition getCursorPosition() { return cursorPosition; } /** * Gets the selection position. * * @return the selection position */ public CursorPosition getSelectionPosition() { return selectionPosition; } /** * Sets the position of the cursor at the specified cooridnates. * * @param x the x coordinate * @param y the y coordinate */ public void setCursorPosition(int x, int y) { cursorPosition.setPosition(x, y); startTimer = System.currentTimeMillis(); } /** * Sets the text color for this {@link UITextField}. * * @param color the color * @return this {@link UITextField} */ public UITextField setTextColor(int color) { this.textColor = color; return this; } /** * Gets the text color. * * @return the text color of this {@link UITextField}. */ public int getTextColor() { return textColor; } /** * Sets whether this {@link UITextField} should automatically select its {@link #text} when focused. * * @param auto the auto * @return this {@link UITextField} */ public UITextField setAutoSelectOnFocus(boolean auto) { autoSelectOnFocus = auto; return this; } /** * Gets the {@link UISlimScrollbar} of this {@link UITextField}. * * @return the scrollbar */ public UISlimScrollbar getScrollbar() { return scrollBar; } // #end getters/setters /** * Builds the lines for this {@link UITextField}. Does nothing if {@link #multiLine} is <code>false</code>. */ protected void buildLines() { lines.clear(); if (text == null || text.length() == 0) { fireEvent(new ContentUpdateEvent<UITextField>(this)); return; } if (!multiLine) { lines.add(text.toString()); return; } String[] texts = text.toString().split("(?<=\r)\n?"); int width = getWidth() - 4; for (String str : texts) lines.addAll(GuiRenderer.wrapText(StatCollector.translateToLocal(str), width, fontScale)); if (text.charAt(text.length() - 1) == '\r') lines.add(""); fireEvent(new ContentUpdateEvent<UITextField>(this)); } /** * Adds text at current cursor position. If some text is selected, it's deleted first. * * @param text the text */ public void addText(String text) { if (selectingText) deleteSelectedText(); int position = cursorPosition.textPosition; String oldValue = this.text.toString(); String newValue = new StringBuilder(oldValue).insert(position, text).toString(); if (!validateText(newValue)) return; if (!fireEvent(new ComponentEvent.ValueChange(this, oldValue, newValue))) return; this.text.insert(position, text); buildLines(); cursorPosition.jumpBy(text.length()); } /** * Checks against if text is valid. * * @param text the text * @return true, if input is valid */ protected boolean validateText(String text) { //TODO : handle text validator return true; } /** * Gets the currently selected text. * * @return the text selected. */ public String getSelectedText() { if (!selectingText) return ""; int start = Math.min(selectionPosition.textPosition, cursorPosition.textPosition); int end = Math.max(selectionPosition.textPosition, cursorPosition.textPosition); return this.text.substring(start, end); } /** * Deletes the text currently selected. */ public void deleteSelectedText() { if (!selectingText) return; int start = Math.min(selectionPosition.textPosition, cursorPosition.textPosition); int end = Math.max(selectionPosition.textPosition, cursorPosition.textPosition); text.delete(start, end); selectingText = false; buildLines(); cursorPosition.jumpTo(start); } /** * Deletes the specified <b>amount</b> of characters. Negative numbers will delete characters left of the cursor.<br> * If text is already selected, delete that text instead. * * @param amount the amount of characters to delete */ public void deleteFromCursor(int amount) { if (!selectingText) { selectingText = true; selectionPosition.set(cursorPosition); selectionPosition.jumpBy(amount); } deleteSelectedText(); } /** * Deletes the text from current cursor position to the next space. * * @param backwards whether to look left for the next space */ public void deleteWord(boolean backwards) { if (!selectingText) { selectingText = true; selectionPosition.set(cursorPosition); cursorPosition.jumpToNextSpace(backwards); } deleteSelectedText(); } /** * Gets the content width. * * @return the content width */ @Override public int getContentWidth() { return getWidth(); } /** * Gets the content height. * * @return the content height */ @Override public int getContentHeight() { return multiLine ? lines.size() * getLineHeight() + 4 : 12; } @Override public float getOffsetX() { return 0; } /** * Sets the offset x. * * @param offsetX the offset x * @param delta the delta */ @Override public void setOffsetX(float offsetX, int delta) { } @Override public float getOffsetY() { return (float) lineOffset / (lines.size() - visibleLines()); } /** * Sets the offset y. * * @param offsetY the offset y * @param delta the delta */ @Override public void setOffsetY(float offsetY, int delta) { lineOffset = Math.round(offsetY / getScrollStep()); lineOffset = Math.max(0, Math.min(lines.size(), lineOffset)); } @Override public float getScrollStep() { float step = (float) 1 / (lines.size() - visibleLines()); return (GuiScreen.isCtrlKeyDown() ? 5 * step : step); } /** * Gets the vertical padding. * * @return the vertical padding */ @Override public int getVerticalPadding() { return 1; } /** * Gets the horizontal padding. * * @return the horizontal padding */ @Override public int getHorizontalPadding() { return 1; } /** * Gets the number of visible lines inside this {@link UITextField}. * * @return the int */ protected int visibleLines() { return multiLine ? (getHeight() - 4) / getLineHeight() : 1; } /** * Called when a cursor is updated.<br> * Offsets the content to make sure the cursor is still visible. */ public void onCursorUpdated() { if (!multiLine) { if (cursorPosition.character < charOffset) charOffset = cursorPosition.character; else if (text.length() != 0) { //charOffset = 0; while (GuiRenderer.getStringWidth(text.substring(charOffset, cursorPosition.textPosition), fontScale) >= width - 4) charOffset++; } } else { if (cursorPosition.line < lineOffset) setLineOffset(cursorPosition.line); else if (cursorPosition.line > lineOffset + visibleLines() - 1) setLineOffset(cursorPosition.line - visibleLines() + 1); } startTimer = System.currentTimeMillis(); } /** * Draws the background. * * @param renderer the renderer * @param mouseX the mouse x * @param mouseY the mouse y * @param partialTick the partial tick */ @Override public void drawBackground(GuiRenderer renderer, int mouseX, int mouseY, float partialTick) { rp.useTexture.reset(); rp.colorMultiplier.reset(); rp.icon.set(isDisabled() ? iconTextfieldDisabled : iconTextfield); renderer.drawShape(shape, rp); } /** * Draws the foreground. * * @param renderer the renderer * @param mouseX the mouse x * @param mouseY the mouse y * @param partialTick the partial tick */ @Override public void drawForeground(GuiRenderer renderer, int mouseX, int mouseY, float partialTick) { if (text.length() != 0) drawText(renderer); if (selectingText && selectionPosition.textPosition != cursorPosition.textPosition) drawSelectionBox(renderer); if (focused) drawCursor(renderer); } /** * Draws the text of this {@link UITextField}. * * @param renderer the renderer */ public void drawText(GuiRenderer renderer) { renderer.setFontScale(fontScale); if (!multiLine) { int end = text.length(); int w = GuiRenderer.getStringWidth(text.substring(charOffset, end), fontScale); while (w > getWidth()) w = GuiRenderer.getStringWidth(text.substring(charOffset, end--), fontScale); renderer.drawText(text.substring(charOffset, end), 2, 2, isDisabled() ? 0xAAAAAA : textColor, true); } else { for (int i = lineOffset; i < lineOffset + visibleLines() && i < lines.size(); i++) { int h = (i - lineOffset) * getLineHeight(); renderer.drawText(lines.get(i), 2, h + 2, isDisabled() ? 0xAAAAAA : textColor, true); } } renderer.setFontScale(1); } /** * Draws the cursor of this {@link UITextField}. * * @param renderer the renderer */ public void drawCursor(GuiRenderer renderer) { long elaspedTime = startTimer - System.currentTimeMillis(); if ((elaspedTime / 500) % 2 != 0) return; if (cursorPosition.line < lineOffset || cursorPosition.line >= lineOffset + visibleLines()) return; GL11.glDisable(GL11.GL_TEXTURE_2D); cursorShape.resetState(); cursorShape.setSize(1, getLineHeight()); cursorShape.setPosition(cursorPosition.getXOffset() + 1, cursorPosition.getYOffset() + 1); rp.useTexture.set(false); rp.colorMultiplier.set(0xD0D0D0); renderer.drawShape(cursorShape, rp); renderer.next(); GL11.glEnable(GL11.GL_TEXTURE_2D); } /** * Draw the selection box of this {@link UITextField}. * * @param renderer the renderer */ public void drawSelectionBox(GuiRenderer renderer) { GL11.glDisable(GL11.GL_TEXTURE_2D); GL11.glEnable(GL11.GL_COLOR_LOGIC_OP); GL11.glLogicOp(GL11.GL_OR_REVERSE); CursorPosition first = cursorPosition.textPosition < selectionPosition.textPosition ? cursorPosition : selectionPosition; CursorPosition last = cursorPosition == first ? selectionPosition : cursorPosition; for (int i = first.line; i <= last.line; i++) { if (i >= lineOffset && i < lineOffset + visibleLines() && i < lines.size()) { int x = 0; int y = (i - lineOffset) * getLineHeight(); int X = GuiRenderer.getStringWidth(lines.get(i), fontScale); if (i == first.line) x = first.getXOffset(); if (i == last.line) X = last.getXOffset(); selectShape.resetState(); selectShape.setSize(Math.min(getWidth() - 2, X) - x, getLineHeight()); selectShape.setPosition(x + 2, y + 1); rp.useTexture.set(false); rp.colorMultiplier.set(0x0000FF); renderer.drawShape(selectShape, rp); } } renderer.next(); GL11.glDisable(GL11.GL_COLOR_LOGIC_OP); GL11.glEnable(GL11.GL_TEXTURE_2D); } /** * Called when a mouse button is pressed over this {@link UITextField}. * * @param event the event */ @Subscribe public void onMousePress(MouseEvent.Press event) { int x = relativeX(event.getX()); int y = relativeY(event.getY()); if (GuiScreen.isShiftKeyDown()) { if (!selectingText) { selectingText = true; selectionPosition.set(cursorPosition); } } else selectingText = false; cursorPosition.setPosition(x, y); // selectAllOnRelease = false; } /** * Called when a mouse button is released over this {@link UITextField}. * * @param event the event */ @Subscribe public void onMouseRelease(MouseEvent.Release event) { if (!autoSelectOnFocus || !selectAllOnRelease) return; selectingText = true; selectionPosition.jumpTo(0); cursorPosition.jumpTo(text.length()); selectAllOnRelease = false; } /** * Called when a mouse button is dragged over this {@link UITextField}. * * @param event the event */ @Subscribe public void onDrag(MouseEvent.Drag event) { if (!this.focused || event.getButton() != MouseButton.LEFT) return; if (!selectingText) { selectingText = true; selectionPosition.set(cursorPosition); } int x = relativeX(event.getX()); int y = relativeY(event.getY()); cursorPosition.setPosition(x, y); selectAllOnRelease = false; } /** * Called when a mouse button is double clicked over this {@link UITextField}. * * @param event the event */ @Subscribe public void onDoubleClick(MouseEvent.DoubleClick event) { //TODO: // int pos = cursorPositionFromX(relativeX(event.getX())); // if (pos > 0 && text.charAt(pos - 1) == ' ') // selectionPosition = pos; // else // selectionPosition = pos + nextSpacePosition(true); // // if (text.charAt(pos) == ' ') // setCursorPosition(pos); // else // setCursorPosition(pos + nextSpacePosition(false) - 1); } /** * Starts selecting text if Shift key is pressed.<br> * Places selection cursor at the current cursor position. */ protected void startSelecting() { if (GuiScreen.isShiftKeyDown()) { if (!selectingText) selectionPosition.set(cursorPosition); selectingText = true; } else selectingText = false; } /** * Called when a key is pressed. * * @param event the event */ @Subscribe public void keyTyped(KeyboardEvent event) { if (!focused) return; char keyChar = event.getKeyChar(); int keyCode = event.getKeyCode(); if (keyCode == Keyboard.KEY_ESCAPE) return; event.cancel(); if (handleCtrlKeyDown(keyCode)) return; switch (keyCode) { case Keyboard.KEY_LEFT: startSelecting(); cursorPosition.shiftLeft(); return; case Keyboard.KEY_RIGHT: startSelecting(); cursorPosition.shiftRight(); return; case Keyboard.KEY_UP: if (multiLine) { startSelecting(); cursorPosition.jumpLine(true); } return; case Keyboard.KEY_DOWN: if (multiLine) { startSelecting(); cursorPosition.jumpLine(false); } return; case Keyboard.KEY_HOME: startSelecting(); cursorPosition.jumpToLineStart(); return; case Keyboard.KEY_END: startSelecting(); cursorPosition.jumpToLineEnd(); return; case Keyboard.KEY_BACK: this.deleteFromCursor(-1); return; case Keyboard.KEY_DELETE: this.deleteFromCursor(1); return; default: if (ChatAllowedCharacters.isAllowedCharacter(keyChar) || (multiLine && keyCode == Keyboard.KEY_RETURN)) this.addText(Character.toString(keyChar)); return; } } /** * Handles the key typed while a control key is pressed. * * @param keyCode the key code * @return true, if successful */ protected boolean handleCtrlKeyDown(int keyCode) { if (!GuiScreen.isCtrlKeyDown()) return false; switch (keyCode) { case Keyboard.KEY_LEFT: startSelecting(); cursorPosition.jumpToNextSpace(true); return true; case Keyboard.KEY_RIGHT: startSelecting(); cursorPosition.jumpToNextSpace(false); return true; case Keyboard.KEY_BACK: this.deleteWord(true); return true; case Keyboard.KEY_DELETE: this.deleteWord(false); return true; case Keyboard.KEY_HOME: startSelecting(); cursorPosition.jumpToBeginning(); return true; case Keyboard.KEY_END: startSelecting(); cursorPosition.jumpToEnd(); return true; case Keyboard.KEY_A: selectingText = true; selectionPosition.jumpToBeginning(); cursorPosition.jumpToEnd(); return true; case Keyboard.KEY_C: GuiScreen.setClipboardString(getSelectedText()); return true; case Keyboard.KEY_V: addText(GuiScreen.getClipboardString()); return true; case Keyboard.KEY_X: GuiScreen.setClipboardString(getSelectedText()); addText(""); return true; default: return false; } } /** * Gets the property string. * * @return the property string */ @Override public String getPropertyString() { return text + " | " + super.getPropertyString(); } /** * The Class CursorPosition. */ protected class CursorPosition { /** The text position. */ protected int textPosition; /** The character. */ protected int character; /** The line. */ protected int line; /** * Sets this {@link CursorPosition} at the same position than <b>position</b>. * * @param position the new position */ public void set(CursorPosition position) { this.textPosition = position.textPosition; this.character = position.character; this.line = position.line; } /** * Sets this {@link CursorPosition} at the specified position in the text. * * @param pos the new cursor position */ public void jumpTo(int pos) { if (text.length() == 0) { textPosition = 0; line = 0; character = 0; return; } if (pos < 0) pos = 0; if (pos >= text.length()) { textPosition = text.length(); line = lines.size() - 1; character = currentLineText().length(); onCursorUpdated(); return; } textPosition = pos; line = 0; if (!multiLine) { character = pos; onCursorUpdated(); return; } while (line < lines.size() && pos >= currentLineText().length()) { pos -= currentLineText().length(); line++; } character = pos; onCursorUpdated(); } /** * Moves this {@link CursorPosition} by a specified amount. * * @param amount the amount */ public void jumpBy(int amount) { jumpTo(textPosition + amount); } /** * Moves this {@link CursorPosition} to the beginning of the text. */ public void jumpToBeginning() { jumpTo(0); } /** * Moves this {@link CursorPosition} to the end of the text. */ public void jumpToEnd() { jumpTo(text.length()); } /** * Moves this {@link CursorPosition} to the beginning of the current line. */ public void jumpToLineStart() { jumpBy(-character); } /** * Moves this {@link CursorPosition} to the end of the current line. */ public void jumpToLineEnd() { textPosition -= character; character = currentLineText().length() - (currentLineText().endsWith("\r") ? 1 : 0); textPosition += character; onCursorUpdated(); } /** * Moves this {@link CursorPosition} one step to the left. If the cursor is at the beginning of the line, it is moved to the end of * the previous line without changing the {@link #textPosition}. */ public void shiftLeft() { if (textPosition == 0) return; if (character == 0 && line > 0) { line--; character = currentLineText().length(); if (currentLineText().endsWith("\r")) { character--; textPosition--; } } else jumpBy(-1); onCursorUpdated(); } /** * Moves this {@link CursorPosition} one step to the right. If the cursor is at the end of the line, it is moved to the start of the * next line whithout changing the {@link #textPosition}. */ public void shiftRight() { if (textPosition == text.length()) return; if (currentLineText().endsWith("\r")) jumpBy(1); else { if (character++ >= currentLineText().length() && line < lines.size() - 1) { line++; character = 0; } else textPosition++; } onCursorUpdated(); } /** * Moves this {@link CursorPosition} to the next space position. * * @param backwards backwards whether to look left of the cursor */ public void jumpToNextSpace(boolean backwards) { int pos = textPosition; int step = backwards ? -1 : 1; pos += step; while (pos > 0 && pos < text.length() && !Character.isWhitespace(text.charAt(pos))) pos += step; jumpTo(pos); } /** * Moves this {@link CursorPosition} to the previous or next line. * * @param backwards if true, jump to the previous line, jump to the next otherwise */ public void jumpLine(boolean backwards) { if (backwards) line = Math.max(0, line - 1); else line = Math.min(line + 1, lines.size() - 1); character = Math.min(character, currentLineText().length() - 1); updateTextPosition(); } /** * Gets the text at the current line. * * @return the text */ private String currentLineText() { if (line < 0 || line >= lines.size()) return ""; return lines.get(line); } /** * Sets this {@link CursorPosition} line and character position based on coordinates inside this {@link UITextField}. * * @param x the X coordinate * @param y the Y coordinate */ public void setPosition(int x, int y) { line = lineFromY(y); character = characterFromX(x); updateTextPosition(); } /** * Update the text position based on {@link #character} and {@link #line}. */ private void updateTextPosition() { textPosition = character; if (!multiLine) return; for (int i = 0; i < line && i < lines.size(); i++) textPosition += lines.get(i).length(); onCursorUpdated(); } /** * Determines the line for given Y coordinate. * * @param y the y coordinate * @return the line number */ private int lineFromY(int y) { return multiLine ? Math.max(0, Math.min(y / getLineHeight() + lineOffset, lines.size() - 1)) : 0; } /** * Determines the character for a given X coordinate. * * @param x the x coordinate * @return position */ private int characterFromX(int x) { if (StringUtils.isEmpty(currentLineText())) return 0; int pos = 0; int width = 0; for (int i = charOffset; i < currentLineText().length(); i++) { int w = GuiRenderer.getCharWidth(currentLineText().charAt(i), fontScale); if (width + ((float) w / 2) > x) return pos + charOffset; width += w; pos++; } return pos + charOffset; } /** * Gets the x offset from the left of this {@link CursorPosition} inside this {@link UITextField}. * * @return the x offset */ public int getXOffset() { if (textPosition == text.length() && multiLine) return GuiRenderer.getStringWidth(currentLineText(), fontScale); if (currentLineText().length() == 0) return 0; if (charOffset >= character) return 0; return GuiRenderer.getStringWidth(currentLineText().substring(charOffset, character), fontScale); } /** * Gets the x offset from the left of this {@link CursorPosition} inside this {@link UITextField}. * * @return the y offset */ public int getYOffset() { return (line - lineOffset) * getLineHeight(); } } }