LibJF/libjf-config-ui-tiny-v1/src/client/java/io/gitlab/jfronny/libjf/config/impl/ui/tiny/EditorScreen.java

578 lines
22 KiB
Java

package io.gitlab.jfronny.libjf.config.impl.ui.tiny;
import com.google.common.collect.Lists;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import io.gitlab.jfronny.commons.ref.R;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.minecraft.SharedConstants;
import net.minecraft.client.font.TextHandler;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.gui.DrawableHelper;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.util.NarratorManager;
import net.minecraft.client.util.SelectionManager;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.client.util.math.Rect2i;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.*;
import net.minecraft.util.Formatting;
import net.minecraft.util.Util;
import net.minecraft.util.math.MathHelper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.apache.commons.lang3.mutable.MutableInt;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Consumer;
public class EditorScreen extends Screen {
private static final int SCROLLBAR_SIZE = 7;
private static final int HEADER_SIZE = 32;
private static final int FOOTER_SIZE = 36;
private static final int PADDING = 14;
private int tickCounter;
private final String initialText;
private String text = "";
private final SelectionManager currentPageSelectionManager = new SelectionManager(
this::getText,
this::setText,
this::getClipboard,
this::setClipboard,
string -> true
);
private long lastClickTime;
private int lastClickIndex = -1;
private double scrollAmount;
private boolean scrolling;
private int getViewportHeight() {
return height - HEADER_SIZE - FOOTER_SIZE - PADDING * 2;
}
private int getViewportWidth() {
return width - SCROLLBAR_SIZE - PADDING * 2;
}
private int getMaxScroll() {
return Math.max(0, getMaxPosition() - getViewportHeight());
}
private int getMaxPosition() {
return getPageContent().lines.length * textRenderer.fontHeight;
}
private int getScrollbarPositionX() {
return width - 7;
}
private void setScrollAmount(double amount) {
this.scrollAmount = MathHelper.clamp(amount, 0, getMaxScroll());
invalidatePageContent();
}
@Nullable
private PageContent pageContent = PageContent.EMPTY;
private final Screen parent;
private final Consumer<String> onSave;
@Nullable private final Text subtitle;
public EditorScreen(@Nullable Text title, @Nullable Text subtitle, @Nullable Screen parent, @Nullable String text, @Nullable Consumer<String> onSave) {
super(title == null ? NarratorManager.EMPTY : title);
this.parent = parent;
this.subtitle = subtitle == null ? null : subtitle.copy().formatted(Formatting.GRAY);
this.initialText = text;
if (text != null) this.text = text;
this.onSave = onSave == null ? R::nop : onSave;
}
public void setText(String text) {
this.text = Objects.requireNonNull(text);
}
public String getText() {
return this.text;
}
private void setClipboard(String clipboard) {
if (this.client != null) {
SelectionManager.setClipboard(this.client, clipboard);
}
}
private String getClipboard() {
return this.client != null ? SelectionManager.getClipboard(this.client) : "";
}
@Override
public void tick() {
super.tick();
++this.tickCounter;
}
@Override
protected void init() {
this.invalidatePageContent();
this.addDrawableChild(
ButtonWidget.builder(ScreenTexts.DONE, button -> quit(true))
.dimensions(this.width / 2 + 4, height - FOOTER_SIZE + 8, 150, 20)
.build()
);
this.addDrawableChild(
ButtonWidget.builder(ScreenTexts.CANCEL, button -> quit(false))
.dimensions(this.width / 2 - 154, height - FOOTER_SIZE + 8, 150, 20)
.build()
);
}
private void quit(boolean save) {
if (save && (this.initialText == null || !this.initialText.equals(this.text))) onSave.accept(text);
close();
}
@Override
public void close() {
Objects.requireNonNull(client).setScreen(parent);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (super.keyPressed(keyCode, scanCode, modifiers)) {
return true;
}
boolean bl = this.keyPressedEditMode(keyCode);
if (bl) {
this.invalidatePageContent();
return true;
}
return false;
}
@Override
public boolean charTyped(char chr, int modifiers) {
if (super.charTyped(chr, modifiers)) {
return true;
}
if (SharedConstants.isValidChar(chr)) {
this.currentPageSelectionManager.insert(Character.toString(chr));
this.invalidatePageContent();
return true;
}
return false;
}
private boolean keyPressedEditMode(int keyCode) {
boolean ctrl = Screen.hasControlDown() && !Screen.hasShiftDown() && !Screen.hasAltDown();
SelectionManager.SelectionType selectionType = Screen.hasControlDown() ? SelectionManager.SelectionType.WORD : SelectionManager.SelectionType.CHARACTER;
return switch (keyCode) {
case 65 -> {
if (ctrl) {
this.currentPageSelectionManager.selectAll();
yield true;
}
yield false;
}
case 67 -> {
if (ctrl) {
this.currentPageSelectionManager.copy();
yield true;
}
yield false;
}
case 83 -> {
if (ctrl) {
quit(true);
yield true;
}
yield false;
}
case 86 -> {
if (ctrl) {
this.currentPageSelectionManager.paste();
yield true;
}
yield false;
}
case 88 -> {
if (ctrl) {
this.currentPageSelectionManager.cut();
yield true;
}
yield false;
}
case 259 -> {
this.currentPageSelectionManager.delete(-1, selectionType);
yield true;
}
case 261 -> {
this.currentPageSelectionManager.delete(1, selectionType);
yield true;
}
case 257, 335 -> {
this.currentPageSelectionManager.insert("\n");
yield true;
}
case 263 -> {
this.currentPageSelectionManager.moveCursor(-1, Screen.hasShiftDown(), selectionType);
yield true;
}
case 262 -> {
this.currentPageSelectionManager.moveCursor(1, Screen.hasShiftDown(), selectionType);
yield true;
}
case 265 -> {
this.moveUpLine();
yield true;
}
case 264 -> {
this.moveDownLine();
yield true;
}
case 268 -> {
this.moveToLineStart();
yield true;
}
case 269 -> {
this.moveToLineEnd();
yield true;
}
default -> false;
};
}
private void moveUpLine() {
this.moveVertically(-1);
}
private void moveDownLine() {
this.moveVertically(1);
}
private void moveVertically(int lines) {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getVerticalOffset(i, lines);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
private void moveToLineStart() {
if (Screen.hasControlDown()) {
this.currentPageSelectionManager.moveCursorToStart(Screen.hasShiftDown());
} else {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getLineStart(i);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
}
private void moveToLineEnd() {
if (Screen.hasControlDown()) {
this.currentPageSelectionManager.moveCursorToEnd(Screen.hasShiftDown());
} else {
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.getPageContent().getLineEnd(i);
this.currentPageSelectionManager.moveCursorTo(j, Screen.hasShiftDown());
}
}
@Override
public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
renderBackground(matrices);
if (subtitle == null) {
drawCenteredTextWithShadow(matrices, textRenderer, title, width / 2, (HEADER_SIZE - textRenderer.fontHeight) / 2, 0xFFFFFF);
} else {
drawCenteredTextWithShadow(matrices, textRenderer, title, width / 2, HEADER_SIZE / 2 - textRenderer.fontHeight, 0xFFFFFF);
drawCenteredTextWithShadow(matrices, textRenderer, subtitle, width / 2, HEADER_SIZE / 2, 0xFFFFFF);
}
this.setFocused(null);
if (client.world == null) {
RenderSystem.setShaderTexture(0, DrawableHelper.OPTIONS_BACKGROUND_TEXTURE);
RenderSystem.setShaderColor(0.125f, 0.125f, 0.125f, 1.0f);
drawTexture(matrices, 0, HEADER_SIZE, width - SCROLLBAR_SIZE, height - FOOTER_SIZE + (int)scrollAmount, width - SCROLLBAR_SIZE, height - HEADER_SIZE - FOOTER_SIZE, 32, 32);
RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
}
enableScissor(0, HEADER_SIZE, width - SCROLLBAR_SIZE, height - FOOTER_SIZE);
PageContent pageContent = this.getPageContent();
for (Line line : pageContent.lines) {
this.textRenderer.draw(matrices, line.text, line.x, line.y, 0xFFFFFFFF);
}
this.drawSelection(matrices, pageContent.selectionRectangles);
this.drawCursor(matrices, pageContent.position, pageContent.atEnd);
disableScissor();
int i = this.getScrollbarPositionX();
int j = i + 6;
int m;
if ((m = this.getMaxScroll()) > 0) {
int n = (getViewportHeight() * getViewportHeight()) / getMaxPosition();
n = MathHelper.clamp(n, 32, getViewportHeight() - 8);
int o = (int)scrollAmount * (getViewportHeight() - n) / m + HEADER_SIZE;
if (o < HEADER_SIZE) o = HEADER_SIZE;
fill(matrices, i, HEADER_SIZE, j, height - FOOTER_SIZE, 0xFF000000);
fill(matrices, i, o, j, o + n, 0xFF808080);
fill(matrices, i, o, j - 1, o + n - 1, 0xFFC0C0C0);
}
super.render(matrices, mouseX, mouseY, delta);
}
private void drawCursor(MatrixStack matrices, Position position, boolean atEnd) {
if (this.tickCounter / 6 % 2 == 0) {
position = this.absolutePositionToScreenPosition(position);
if (!atEnd) {
DrawableHelper.fill(matrices, position.x, position.y - 1, position.x + 1, position.y + this.textRenderer.fontHeight, 0xFFFFFFFF);
} else {
this.textRenderer.draw(matrices, "_", position.x, position.y, 0);
}
}
}
private void drawSelection(MatrixStack matrices, Rect2i[] selectionRectangles) {
RenderSystem.enableColorLogicOp();
RenderSystem.logicOp(GlStateManager.LogicOp.OR_REVERSE);
for (Rect2i rect2i : selectionRectangles) {
int i = rect2i.getX();
int j = rect2i.getY();
int k = i + rect2i.getWidth();
int l = j + rect2i.getHeight();
fill(matrices, i, j, k, l, 0xFF0000FF);
}
RenderSystem.disableColorLogicOp();
}
private Position screenPositionToAbsolutePosition(Position position) {
return new Position(position.x - PADDING, position.y - HEADER_SIZE - PADDING + (int)scrollAmount);
}
private Position absolutePositionToScreenPosition(Position position) {
return new Position(position.x + PADDING, position.y + HEADER_SIZE + PADDING - (int)scrollAmount);
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (super.mouseClicked(mouseX, mouseY, button)) {
return true;
}
scrolling = button == 0 && mouseX >= getScrollbarPositionX();
if (button == 0 && !scrolling) {
long l = Util.getMeasuringTimeMs();
int i = this.getPageContent().getCursorPosition(this.textRenderer, this.screenPositionToAbsolutePosition(new Position((int) mouseX, (int) mouseY)));
if (i >= 0) {
if (i == this.lastClickIndex && l - this.lastClickTime < 250L) {
if (!this.currentPageSelectionManager.isSelecting()) {
this.selectCurrentWord(i);
} else {
this.currentPageSelectionManager.selectAll();
}
} else {
this.currentPageSelectionManager.moveCursorTo(i, Screen.hasShiftDown());
}
this.invalidatePageContent();
}
this.lastClickIndex = i;
this.lastClickTime = l;
}
return true;
}
private void selectCurrentWord(int cursor) {
String string = this.getText();
this.currentPageSelectionManager.setSelection(TextHandler.moveCursorByWords(string, -1, cursor, false), TextHandler.moveCursorByWords(string, 1, cursor, false));
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
if (super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY)) {
return true;
}
if (button == 0) {
if (scrolling) {
if (mouseY < HEADER_SIZE) setScrollAmount(0);
else if (mouseY > height - FOOTER_SIZE) setScrollAmount(getMaxScroll());
else {
double d = Math.max(1, getMaxScroll());
int i = getViewportHeight();
int j = MathHelper.clamp((int)((float)(i * i) / (float)this.getMaxPosition()), 32, i - 8);
double e = Math.max(1d, d / (i - j));
setScrollAmount(scrollAmount + deltaY * e);
}
} else {
int i = this.getPageContent().getCursorPosition(this.textRenderer, this.screenPositionToAbsolutePosition(new Position((int)mouseX, (int)mouseY)));
this.currentPageSelectionManager.moveCursorTo(i, true);
this.invalidatePageContent();
}
}
return true;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
setScrollAmount(scrollAmount - amount * textRenderer.fontHeight * 2);
return true;
}
private PageContent getPageContent() {
if (this.pageContent == null) {
this.pageContent = this.createPageContent();
}
return this.pageContent;
}
private void invalidatePageContent() {
this.pageContent = null;
}
private PageContent createPageContent() {
String content = this.getText();
if (content.isEmpty()) {
return PageContent.EMPTY;
}
int i = this.currentPageSelectionManager.getSelectionStart();
int j = this.currentPageSelectionManager.getSelectionEnd();
IntArrayList intList = new IntArrayList();
ArrayList<Line> list = Lists.newArrayList();
MutableInt lineIndexM = new MutableInt();
MutableBoolean mutableBoolean = new MutableBoolean();
TextHandler textHandler = this.textRenderer.getTextHandler();
textHandler.wrapLines(content, getViewportWidth(), Style.EMPTY, true, (style, start, end) -> {
int lineIndex = lineIndexM.getAndIncrement();
String string = content.substring(start, end);
mutableBoolean.setValue(string.endsWith("\n"));
String string2 = StringUtils.stripEnd(string, " \n");
int y = lineIndex * this.textRenderer.fontHeight;
Position position = this.absolutePositionToScreenPosition(new Position(0, y));
intList.add(start);
list.add(new Line(style, string2, position.x, position.y));
});
int[] is = intList.toIntArray();
boolean bl = i == content.length();
int l;
Position position;
if (bl && mutableBoolean.isTrue()) {
position = new Position(0, list.size() * this.textRenderer.fontHeight);
} else {
int k = getLineFromOffset(is, i);
l = this.textRenderer.getWidth(content.substring(is[k], i));
position = new Position(l, k * this.textRenderer.fontHeight);
}
ArrayList<Rect2i> list2 = Lists.newArrayList();
if (i != j) {
int o;
l = Math.min(i, j);
int m = Math.max(i, j);
int n = getLineFromOffset(is, l);
if (n == (o = getLineFromOffset(is, m))) {
int p = n * this.textRenderer.fontHeight;
int q = is[n];
list2.add(this.getLineSelectionRectangle(content, textHandler, l, m, p, q));
} else {
int p = n + 1 > is.length ? content.length() : is[n + 1];
list2.add(this.getLineSelectionRectangle(content, textHandler, l, p, n * this.textRenderer.fontHeight, is[n]));
for (int q = n + 1; q < o; ++q) {
int r = q * this.textRenderer.fontHeight;
String string2 = content.substring(is[q], is[q + 1]);
int s = (int)textHandler.getWidth(string2);
list2.add(this.getRectFromCorners(new Position(0, r), new Position(s, r + this.textRenderer.fontHeight)));
}
list2.add(this.getLineSelectionRectangle(content, textHandler, is[o], m, o * this.textRenderer.fontHeight, is[o]));
}
}
return new PageContent(content, position, bl, is, list.toArray(new Line[0]), list2.toArray(new Rect2i[0]));
}
static int getLineFromOffset(int[] lineStarts, int position) {
int i = Arrays.binarySearch(lineStarts, position);
if (i < 0) {
return -(i + 2);
}
return i;
}
private Rect2i getLineSelectionRectangle(String string, TextHandler handler, int selectionStart, int selectionEnd, int lineY, int lineStart) {
String string2 = string.substring(lineStart, selectionStart);
String string3 = string.substring(lineStart, selectionEnd);
Position position = new Position((int)handler.getWidth(string2), lineY);
Position position2 = new Position((int)handler.getWidth(string3), lineY + this.textRenderer.fontHeight);
return this.getRectFromCorners(position, position2);
}
private Rect2i getRectFromCorners(Position start, Position end) {
Position position = this.absolutePositionToScreenPosition(start);
Position position2 = this.absolutePositionToScreenPosition(end);
int i = Math.min(position.x, position2.x);
int j = Math.max(position.x, position2.x);
int k = Math.min(position.y, position2.y);
int l = Math.max(position.y, position2.y);
return new Rect2i(i, k, j - i, l - k);
}
static class PageContent {
static final PageContent EMPTY = new PageContent("", new Position(0, 0), true, new int[]{0}, new Line[]{new Line(Style.EMPTY, "", 0, 0)}, new Rect2i[0]);
private final String pageContent;
final Position position;
final boolean atEnd;
private final int[] lineStarts;
final Line[] lines;
final Rect2i[] selectionRectangles;
public PageContent(String pageContent, Position position, boolean atEnd, int[] lineStarts, Line[] lines, Rect2i[] selectionRectangles) {
this.pageContent = pageContent;
this.position = position;
this.atEnd = atEnd;
this.lineStarts = lineStarts;
this.lines = lines;
this.selectionRectangles = selectionRectangles;
}
public int getCursorPosition(TextRenderer renderer, Position position) {
int i = position.y / renderer.fontHeight;
if (i < 0) {
return 0;
}
if (i >= this.lines.length) {
return this.pageContent.length();
}
Line line = this.lines[i];
return this.lineStarts[i] + renderer.getTextHandler().getTrimmedLength(line.content, position.x, line.style);
}
public int getVerticalOffset(int position, int lines) {
int m;
int i = getLineFromOffset(this.lineStarts, position);
int j = i + lines;
if (0 <= j && j < this.lineStarts.length) {
int k = position - this.lineStarts[i];
int l = this.lines[j].content.length();
m = this.lineStarts[j] + Math.min(k, l);
} else {
m = position;
}
return m;
}
public int getLineStart(int position) {
int i = getLineFromOffset(this.lineStarts, position);
return this.lineStarts[i];
}
public int getLineEnd(int position) {
int i = getLineFromOffset(this.lineStarts, position);
return this.lineStarts[i] + this.lines[i].content.length();
}
}
record Line(Style style, String content, Text text, int x, int y) {
public Line(Style style, String content, int x, int y) {
this(style, content, Text.literal(content).setStyle(style), x, y);
}
}
record Position(int x, int y) { }
}