-
Notifications
You must be signed in to change notification settings - Fork 11
Seamless Page Overflow V.2 #123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
72bffb8
048aed3
a600d58
9fc0dd0
6765249
4f7cf90
18f8853
144ca1a
ed85c28
9a97098
52ec993
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,8 @@ public class ScribbleConfig extends ReflectedConfig { | |
| public Value<Boolean> copyFormattingCodes = value(true); | ||
| public Value<Integer> editHistorySize = value(32) | ||
| .range(8, 128, 1); | ||
| public Value<Boolean> overflowWhenTyping = value(false); | ||
| public Value<PasteBehavior> pasteBehavior = value(PasteBehavior.DENY); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The name "Paste behaviour" doesn't really indicate its connection to overflowing. Maybe "Overflow when pasting" to match the other option? |
||
|
|
||
| @Category("miscellaneous") | ||
| public Value<Boolean> openVanillaBookScreenOnShift = value(false); | ||
|
|
@@ -43,4 +45,10 @@ public enum ShowActionButtons { | |
| WHEN_EDITING, | ||
| NEVER, | ||
| } | ||
|
|
||
| public enum PasteBehavior { | ||
| DENY, | ||
| FIT_PAGE, | ||
| OVERFLOW, | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -508,6 +508,19 @@ public String getAsFormattedString() { | |
| return out.toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Remove carriage return characters (\r) from the text. This is needed because | ||
| * Windows line endings (CRLF) contain \r characters that Minecraft books display | ||
| * as visible symbols instead of treating them as whitespace. | ||
| * | ||
| * @return a new RichText with all carriage returns removed. | ||
| */ | ||
| public RichText filterCarriageReturns() { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this just be filtered out the raw clipboard text when pasting, instead of after having processed everything already? |
||
| String plain = this.getPlainText(); | ||
| if (!plain.contains("\r")) return this; | ||
| return RichText.fromFormattedString(this.getAsFormattedString().replace("\r", "")); | ||
| } | ||
|
|
||
| /** | ||
| * Get the rich text as a vanilla {@link MutableComponent}. Note that this text content is valid for | ||
| * this client only! | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package me.chrr.scribble.book; | ||
|
|
||
| import me.chrr.scribble.history.HistoryListener; | ||
| import me.chrr.scribble.history.command.OverflowCommand; | ||
| import net.minecraft.ChatFormatting; | ||
| import net.minecraft.client.gui.Font; | ||
| import net.minecraft.network.chat.Style; | ||
| import org.jspecify.annotations.NullMarked; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
| import java.util.function.Consumer; | ||
|
|
||
| @NullMarked | ||
| public class TextOverflowHandler { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two things here:
|
||
| private static final int PAGE_WIDTH = 114; | ||
| private static final int LINE_LIMIT = 14; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't these already exist in other places: |
||
|
|
||
| private final HistoryListener listener; | ||
| private final Font font; | ||
| private final Consumer<OverflowCommand> onCommand; | ||
|
|
||
| public TextOverflowHandler(HistoryListener listener, Font font, Consumer<OverflowCommand> onCommand) { | ||
| this.listener = listener; | ||
| this.font = font; | ||
| this.onCommand = onCommand; | ||
| } | ||
|
|
||
| public boolean insertWithOverflow(int page, RichText text, int cursor, RichText insert, | ||
| @Nullable ChatFormatting color, Set<ChatFormatting> modifiers) { | ||
| int maxPages = 100 - listener.getTotalPages() + 1; | ||
| if (cursor != text.getLength() || maxPages < 1) return false; | ||
|
|
||
| // Truncate insert to max pages × ~400 chars/page (conservative estimate) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI? |
||
| insert = insert.subText(0, Math.min(insert.getLength(), maxPages * 400)); | ||
|
|
||
| // Combine text + insert first, then split with word-wrapping | ||
| RichText combined = text.insert(cursor, insert); | ||
|
|
||
| List<RichText> pages = new ArrayList<>(); | ||
| RichText remaining = combined; | ||
| for (; remaining.getLength() > 0 && pages.size() < maxPages; ) { | ||
| int len = findFittingLength(remaining); | ||
| pages.add(remaining.subText(0, len)); | ||
| remaining = remaining.subText(len, remaining.getLength()); | ||
| } | ||
|
|
||
| // Reject only if no overflow pages were created (at page limit with no room) | ||
| // Allow truncation when overflow pages were successfully added | ||
| if (remaining.getLength() > 0 && pages.size() == 1) { | ||
| return false; | ||
| } | ||
|
|
||
| OverflowCommand cmd = new OverflowCommand(page, listener.getPageContent(page), pages); | ||
| cmd.execute(listener); | ||
| onCommand.accept(cmd); | ||
| return true; | ||
| } | ||
|
|
||
| private int findFittingLength(RichText text) { | ||
| if (!wouldOverflow(text)) return text.getLength(); | ||
|
|
||
| // Binary search for max chars that fit on one page | ||
| int lo = 1, hi = text.getLength(), best = 1; | ||
| while (lo <= hi) { | ||
| int mid = (lo + hi) / 2; | ||
| if (!wouldOverflow(text.subText(0, mid))) { best = mid; lo = mid + 1; } | ||
| else hi = mid - 1; | ||
| } | ||
|
|
||
| // Find last space in the fitted text for word wrapping | ||
| String plain = text.subText(0, best).getPlainText(); | ||
| int lastNewline = plain.lastIndexOf('\n'); | ||
| int searchStart = Math.max(0, lastNewline); // Only look for space after last newline | ||
| int lastSpace = plain.lastIndexOf(' ', best - 1); | ||
|
|
||
| // Only use space if it's on the last visual line (after last newline) | ||
| if (lastSpace > searchStart) { | ||
| return lastSpace + 1; | ||
| } | ||
| return best; | ||
| } | ||
|
|
||
| private boolean wouldOverflow(RichText text) { | ||
| return font.getSplitter().splitLines(text, PAGE_WIDTH, Style.EMPTY).size() > LINE_LIMIT; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package me.chrr.scribble.gui.edit; | ||
|
|
||
| import me.chrr.scribble.book.RichText; | ||
| import net.minecraft.ChatFormatting; | ||
| import org.jspecify.annotations.NullMarked; | ||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * Interface for handling text overflow situations in the book editor. | ||
| * This is called when text operations would cause the page to overflow. | ||
| */ | ||
| @NullMarked | ||
| public interface OverflowHandler { | ||
| /** | ||
| * Called when typing or pasting would overflow the current page. | ||
| * | ||
| * @param currentText the current text on the page | ||
| * @param cursor the cursor position | ||
| * @param insert the text to insert | ||
| * @param color the current color | ||
| * @param modifiers the current modifiers | ||
| * @return true if the overflow was handled, false if the operation should be rejected | ||
| */ | ||
| boolean handleOverflow(RichText currentText, int cursor, RichText insert, | ||
| @Nullable ChatFormatting color, Set<ChatFormatting> modifiers); | ||
|
|
||
| /** | ||
| * Called when Enter is pressed at the end of a page. | ||
| * | ||
| * @return true if a new page was created | ||
| */ | ||
| boolean handleEnterAtEnd(); | ||
|
|
||
| /** | ||
| * Called when Backspace is pressed on a completely empty page. | ||
| * | ||
| * @return true if the page was deleted | ||
| */ | ||
| boolean handleBackspaceOnEmpty(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,5 +16,14 @@ public interface HistoryListener { | |
|
|
||
| void insertPageAt(int page, @Nullable RichText content); | ||
|
|
||
| void deletePage(int page); | ||
| /** @param navigateDirection negative = go left, 0 or positive = stay/go right */ | ||
| void deletePage(int page, int navigateDirection); | ||
|
|
||
| int getTotalPages(); | ||
|
|
||
| RichText getPageContent(int page); | ||
|
|
||
| void setPageContent(int page, RichText content); | ||
|
|
||
| void refreshPages(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most of these methods feel unneccessary, this can all be done using
I might be missing something here, do tell me, but I'd rather avoid having multiple ways to do the same thing. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like mentioned in the previous PR (#92 (comment)), I think it would be nice to have an enum here with three options:
-to fill up the last line).