mirror of
https://gitlab.com/biskuteri-cafe/JKomasto2.git
synced 2024-11-20 05:14:50 +01:00
Fixed RichTextPane somewhat.
Switched to BreakIterator.
This commit is contained in:
parent
7ede5e1290
commit
695d1057a2
@ -32,6 +32,8 @@ import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.io.IOException;
|
||||
import cafe.biskuteri.hinoki.Tree;
|
||||
import java.text.BreakIterator;
|
||||
import java.util.Locale;
|
||||
|
||||
|
||||
class
|
||||
@ -373,8 +375,19 @@ implements ActionListener {
|
||||
}
|
||||
if (node.key.equals("text"))
|
||||
{
|
||||
for (String word: node.value.split(" "))
|
||||
b = b.text(word).spacer(" ");
|
||||
BreakIterator it = BreakIterator.getWordInstance(Locale.ROOT);
|
||||
String text = node.value;
|
||||
it.setText(text);
|
||||
int start = it.first(), end = it.next();
|
||||
while (end != BreakIterator.DONE)
|
||||
{
|
||||
String word = text.substring(start, end);
|
||||
char c = word.isEmpty() ? ' ' : word.charAt(0);
|
||||
boolean w = Character.isWhitespace(c);
|
||||
b = w ? b.spacer(word) : b.text(word);
|
||||
start = end;
|
||||
end = it.next();
|
||||
}
|
||||
}
|
||||
if (node.key.equals("emoji"))
|
||||
{
|
||||
|
@ -53,13 +53,12 @@ RichTextPane extends JComponent {
|
||||
public static List<Segment>
|
||||
layout(List<Segment> text, FontMetrics fm, int width)
|
||||
{
|
||||
if (width < fm.getMaxAdvance()) return new LinkedList<>();
|
||||
|
||||
List<Segment> copy = new LinkedList<>();
|
||||
for (Segment segment: text) copy.add(segment.clone());
|
||||
text = copy;
|
||||
ListIterator<Segment> cursor = text.listIterator();
|
||||
int dy = fm.getHeight(), x = 0, y = dy;
|
||||
int x = 0, y = fm.getAscent();
|
||||
int dy = fm.getHeight();
|
||||
while (cursor.hasNext())
|
||||
{
|
||||
Segment curr = cursor.next();
|
||||
@ -82,8 +81,10 @@ RichTextPane extends JComponent {
|
||||
dx = 0;
|
||||
}
|
||||
|
||||
// If can readily fit, just do so.
|
||||
if (x + dx < width || curr.spacer) {
|
||||
boolean fits = x + dx < width;
|
||||
|
||||
if (fits || curr.spacer)
|
||||
{
|
||||
curr.x = x;
|
||||
curr.y = y;
|
||||
x += dx;
|
||||
@ -94,47 +95,68 @@ RichTextPane extends JComponent {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If image, or text that isn't long, just break.
|
||||
if (curr.image != null || dx < width / 3) {
|
||||
boolean tooLong = dx > width;
|
||||
boolean canFitChar = width >= fm.getMaxAdvance();
|
||||
boolean splittable = curr.image == null;
|
||||
/*
|
||||
* A bit of redundancy in my conditions, but the point is
|
||||
* to exactly express the triggers in my mental model.
|
||||
* The conditions should read more like English.
|
||||
*/
|
||||
|
||||
if (!tooLong || (tooLong && !splittable))
|
||||
{
|
||||
curr.x = 0;
|
||||
curr.y = y += dy;
|
||||
x = dx;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Greedily split string to fit into line.
|
||||
int offset = splitForFit(curr.text, fm, width - x);
|
||||
if (offset == 0) {
|
||||
cursor.add(curr); cursor.previous();
|
||||
y += dy;
|
||||
x = dx;
|
||||
continue;
|
||||
assert tooLong && splittable;
|
||||
|
||||
String s = curr.text;
|
||||
int splitOffset;
|
||||
for (splitOffset = 0; splitOffset < s.length(); ++splitOffset)
|
||||
{
|
||||
String substring = s.substring(0, splitOffset + 1);
|
||||
if (fm.stringWidth(substring) > width) break;
|
||||
}
|
||||
Segment next = new Segment();
|
||||
next.text = curr.text.substring(offset);
|
||||
next.link = curr.link;
|
||||
cursor.add(next); cursor.previous();
|
||||
curr.text = curr.text.substring(0, offset);
|
||||
curr.x = x;
|
||||
curr.y = y;
|
||||
if (splitOffset == 0) splitOffset = 1;
|
||||
/*
|
||||
* I force a split even if our width supports no characters.
|
||||
* Because if I don't split, the only alternatives to infinitely
|
||||
* looping downwards is to emplace this segment or ignore it.
|
||||
*/
|
||||
Segment fitted = new Segment();
|
||||
fitted.text = s.substring(0, splitOffset);
|
||||
fitted.link = curr.link;
|
||||
fitted.x = x;
|
||||
fitted.y = y;
|
||||
cursor.add(fitted);
|
||||
curr.text = s.substring(splitOffset);
|
||||
y += dy;
|
||||
x = 0;
|
||||
cursor.add(curr); cursor.previous();
|
||||
/*
|
||||
* I had to use a stack and return a new list because,
|
||||
* splitting can turn a long segment into several spread
|
||||
* over different lines. Here curr becomes the "after-split"
|
||||
* and I push it back to the stack.
|
||||
*
|
||||
* If #layout wasn't a separate method, but rather only for
|
||||
* graphical painting. Then I don't need a stack nor return
|
||||
* a new list. I iterate over the given one, filling the
|
||||
* nodes' geometric information, if a split occurs I save the
|
||||
* "after-split" in a variable, which in the next iteration
|
||||
* I use as curr instead of list.next(). The caller doesn't
|
||||
* need to know the geometry of these intermediate segments.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// - -%- -
|
||||
|
||||
private static int
|
||||
splitForFit(String s, FontMetrics fm, int width)
|
||||
{
|
||||
int max = 0;
|
||||
for (int o = 1; o < s.length(); max = o++)
|
||||
if (fm.stringWidth(s.substring(0, o)) > width) break;
|
||||
return max;
|
||||
}
|
||||
|
||||
// ---%-@-%---
|
||||
|
||||
public static class
|
||||
|
Loading…
Reference in New Issue
Block a user