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.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.util.NarratorManager; import net.minecraft.client.util.SelectionManager; 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 ScreenWithSaveHook { 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 onSave; @Nullable private final Text subtitle; public EditorScreen(@Nullable Text title, @Nullable Text subtitle, @Nullable Screen parent, @Nullable String text, @Nullable Consumer 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); saveHook.run(); } 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(DrawContext context, int mouseX, int mouseY, float delta) { super.render(context, mouseX, mouseY, delta); if (subtitle == null) { context.drawCenteredTextWithShadow(textRenderer, title, width / 2, (HEADER_SIZE - textRenderer.fontHeight) / 2, 0xFFFFFF); } else { context.drawCenteredTextWithShadow(textRenderer, title, width / 2, HEADER_SIZE / 2 - textRenderer.fontHeight, 0xFFFFFF); context.drawCenteredTextWithShadow(textRenderer, subtitle, width / 2, HEADER_SIZE / 2, 0xFFFFFF); } this.setFocused(null); final int maxScroll = this.getMaxScroll(); final boolean showScrollbar = maxScroll > 0; if (client.world == null) { RenderSystem.setShaderColor(0.125f, 0.125f, 0.125f, 1.0f); context.drawTexture(Screen.OPTIONS_BACKGROUND_TEXTURE, 0, HEADER_SIZE, showScrollbar ? width - SCROLLBAR_SIZE : width, 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); } context.enableScissor(0, HEADER_SIZE, width - SCROLLBAR_SIZE, height - FOOTER_SIZE); PageContent pageContent = this.getPageContent(); for (Line line : pageContent.lines) { context.drawText(textRenderer, line.text, line.x, line.y, 0xFFFFFFFF, false); } this.drawSelection(context, pageContent.selectionRectangles); this.drawCursor(context, pageContent.position); context.disableScissor(); int scrollbarX = this.getScrollbarPositionX(); int scrollbarXEnd = scrollbarX + 6; if (showScrollbar) { int n = (getViewportHeight() * getViewportHeight()) / getMaxPosition(); n = MathHelper.clamp(n, 32, getViewportHeight() - 8); int o = (int)scrollAmount * (getViewportHeight() - n) / maxScroll + HEADER_SIZE; if (o < HEADER_SIZE) o = HEADER_SIZE; context.fill(scrollbarX, HEADER_SIZE, scrollbarXEnd, height - FOOTER_SIZE, 0xFF000000); context.fill(scrollbarX, o, scrollbarXEnd, o + n, 0xFF808080); context.fill(scrollbarX, o, scrollbarXEnd - 1, o + n - 1, 0xFFC0C0C0); } } private void drawCursor(DrawContext context, Position position) { if (this.tickCounter / 6 % 2 == 0) { position = this.absolutePositionToScreenPosition(position); context.fill(position.x, position.y - 1, position.x + 1, position.y + this.textRenderer.fontHeight, 0xFFFFFFFF); } } private void drawSelection(DrawContext context, Rect2i[] selectionRectangles) { for (Rect2i rect2i : selectionRectangles) { int i = rect2i.getX(); int j = rect2i.getY(); int k = i + rect2i.getWidth(); int l = j + rect2i.getHeight(); context.fill(RenderLayer.getGuiTextHighlight(), i, j, k, l, 0xFF0000FF); } } 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 horizontalAmount, double verticalAmount) { setScrollAmount(scrollAmount - verticalAmount * 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 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 atEnd = i == content.length(); int l; Position position; if (atEnd && 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 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, 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), new int[]{0}, new Line[]{new Line(Style.EMPTY, "", 0, 0)}, new Rect2i[0]); private final String pageContent; final Position position; private final int[] lineStarts; final Line[] lines; final Rect2i[] selectionRectangles; public PageContent(String pageContent, Position position, int[] lineStarts, Line[] lines, Rect2i[] selectionRectangles) { this.pageContent = pageContent; this.position = position; 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) { } }