diff --git a/PostWindow.java b/PostWindow.java index d7ae324..34d10a9 100755 --- a/PostWindow.java +++ b/PostWindow.java @@ -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")) { diff --git a/RichTextPane.java b/RichTextPane.java index 8e8a5dd..4f32ca6 100644 --- a/RichTextPane.java +++ b/RichTextPane.java @@ -53,13 +53,12 @@ RichTextPane extends JComponent { public static List layout(List text, FontMetrics fm, int width) { - if (width < fm.getMaxAdvance()) return new LinkedList<>(); - List copy = new LinkedList<>(); for (Segment segment: text) copy.add(segment.clone()); text = copy; ListIterator 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