/* copyright This file is part of JKomasto2. Written in 2022 by Usawashi This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . copyright */ import javax.swing.JComponent; import java.text.AttributedString; import java.text.AttributedCharacterIterator; import java.awt.Graphics; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Image; import java.awt.geom.Rectangle2D; import java.awt.font.TextAttribute; import java.awt.event.ComponentListener; import java.awt.event.ComponentEvent; import java.util.List; import java.util.ArrayList; import cafe.biskuteri.hinoki.Tree; class RichTextPane2 extends JComponent implements ComponentListener { private AttributedString text; // ---%-@-%--- public void setText(Tree html, Tree emojiMap) { Tree commands = turnIntoCommands(html); class AStrSegment { String text; int offset; Object[] values = new Object[Attribute.COUNT]; /* { values[3] = (Boolean)true; values[4] = (Integer)0; values[5] = (Boolean)true; values[6] = (Boolean)false; } */ } List segments = new ArrayList<>(); int offset = 0; for (Tree command: commands) { if (command.key.equals("text")) { StringBuilder b = new StringBuilder(); Boolean cibl = null; Boolean cwhi = null; for (char c: command.value.toCharArray()) { Boolean ibl = isBasicLatin(c); Boolean whi = Character.isWhitespace(c); if (!ibl.equals(cibl) || !whi.equals(cwhi)) { if (b.length() > 0) { assert cibl != null && cwhi != null; AStrSegment s = new AStrSegment(); s.offset = offset; s.text = b.toString(); s.values[3] = cibl; s.values[6] = cwhi; segments.add(s); offset += s.text.length(); b.delete(0, b.length()); } cibl = ibl; cwhi = whi; } b.append(c); } if (b.length() > 0) { AStrSegment s = new AStrSegment(); s.offset = offset; s.text = b.toString(); s.values[3] = cibl; s.values[6] = cwhi; segments.add(s); offset += s.text.length(); } } else if (command.key.equals("emoji")) { AStrSegment s = new AStrSegment(); s.offset = offset; s.values[3] = true; s.values[6] = false; String shortcode = command.value; String url = null; Tree m = emojiMap.get(shortcode); if (m != null) url = m.value; Image img = ImageApi.remote(url); if (img != null) { s.text = " "; s.values[0] = img; s.values[1] = shortcode; segments.add(s); offset += 1; } else { s.text = shortcode; s.values[0] = null; s.values[1] = null; segments.add(s); offset += shortcode.length(); } } else if (command.key.equals("link")) { AStrSegment s = new AStrSegment(); s.offset = offset; s.text = command.value; s.values[2] = command.get("url").value; s.values[3] = true; s.values[6] = false; /* * Technically we're supposed to treat * the anchor text like a text node. * As in, it could be non-Basic-Latin.. * I'll be Mastodon-specific again, and * assume it's a URL or some @ string. */ } } AttributedString astr; StringBuilder b = new StringBuilder(); for (AStrSegment segment: segments) { b.append(segment.text); } astr = new AttributedString(b.toString()); for (AStrSegment segment: segments) { Object[] v = segment.values; astr.addAttribute( Attribute.IMAGE, segment.values[0], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.ALT, segment.values[1], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.LINK, segment.values[2], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.BASICLATIN, segment.values[3], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.Y, segment.values[4], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.OFFSCREEN, segment.values[5], segment.offset, segment.offset + segment.text.length() ); astr.addAttribute( Attribute.WHITESPACE, segment.values[6], segment.offset, segment.offset + segment.text.length() ); } this.text = astr; componentResized(null); } // - -%- - public void componentResized(ComponentEvent eC) { int w = getWidth(), h = getHeight(); // We're going to evaluate the // line and off-screen attributes. FontMetrics fm = getFontMetrics(getFont()); Graphics g = getGraphics(); int x = 0, y = fm.getAscent(); AttributedCharacterIterator it; it = text.getIterator(); while (it.getIndex() < it.getEndIndex()) { int start = it.getIndex(); int end = it.getRunLimit(); Image img = (Image) it.getAttribute(Attribute.IMAGE); Boolean ibl = (Boolean) it.getAttribute(Attribute.BASICLATIN); Boolean whi = (Boolean) it.getAttribute(Attribute.WHITESPACE); assert ibl != null; assert whi != null; if (img != null) { int ow = img.getWidth(this); int oh = img.getHeight(this); int nh = fm.getAscent() + fm.getDescent(); int nw = ow * nh/oh; if (x + nw > w) { y += fm.getAscent() + fm.getDescent(); x = nw; } text.addAttribute( Attribute.Y, (Integer)y, start, end ); text.addAttribute( Attribute.OFFSCREEN, (Boolean)(y > h), start, end ); it.setIndex(end); } else { int p, xOff = 0; for (p = end; p > start; --p) { Rectangle2D r; r = fm.getStringBounds(it, start, p, g); xOff = (int)r.getWidth(); if (x + xOff < w) break; } if (p == end || whi) { x += xOff; text.addAttribute( Attribute.Y, (Integer)y, start, end ); text.addAttribute( Attribute.OFFSCREEN, (Boolean)(y > h), start, end ); it.setIndex(end); } else if (p <= start) { y += fm.getAscent() + fm.getDescent(); x = xOff; text.addAttribute( Attribute.Y, (Integer)y, start, end ); text.addAttribute( Attribute.OFFSCREEN, (Boolean)(y > h), start, end ); it.setIndex(end); } else { text.addAttribute( Attribute.Y, (Integer)y, start, p ); text.addAttribute( Attribute.OFFSCREEN, (Boolean)(y > h), start, p ); y += fm.getAscent() + fm.getDescent(); x = 0; it.setIndex(p); } } } text.addAttribute(TextAttribute.FONT, getFont()); repaint(); } protected void paintComponent(Graphics g) { int w = getWidth(), h = getHeight(); g.clearRect(0, 0, w, h); FontMetrics fm = g.getFontMetrics(); AttributedCharacterIterator it; it = text.getIterator(); ((java.awt.Graphics2D)g).setRenderingHint( java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON ); int x = 0, y = fm.getAscent(); while (it.getIndex() < it.getEndIndex()) { int start = it.getIndex(); int end = it.getRunLimit(); Image img = (Image) it.getAttribute(Attribute.IMAGE); Boolean ibl = (Boolean) it.getAttribute(Attribute.BASICLATIN); Integer ny = (Integer) it.getAttribute(Attribute.Y); if (ny > y) { y = ny; x = 0; } if (img != null) { int ow = img.getWidth(this); int oh = img.getHeight(this); int nh = fm.getAscent() + fm.getDescent(); int nw = ow * nh/oh; int iy = y + fm.getDescent() - nh; g.drawImage(img, x, iy, nw, nh, this); x += nw; } else { Rectangle2D r; r = fm.getStringBounds(it, start, end, g); AttributedCharacterIterator sit; sit = text.getIterator(null, start, end); g.drawString(sit, x, y); x += (int)r.getWidth(); } it.setIndex(end); } } public void componentMoved(ComponentEvent eC) { } public void componentShown(ComponentEvent eC) { } public void componentHidden(ComponentEvent eC) { } // - -%- - private static Boolean isBasicLatin(char c) { return true; } private static String toText(Tree node) { Tree children = node.get("children"); if (children == null) { boolean text = node.key.equals("text"); boolean emoji = node.key.equals("emoji"); assert text || emoji; return node.value; } StringBuilder b = new StringBuilder(); for (Tree child: children) { b.append(toText(child)); } return b.toString(); } private static Tree turnIntoCommands(Tree tag) { assert tag.key.equals("tag"); Tree returnee = new Tree(); String tagName = tag.get(0).key; Tree children = tag.get("children"); if (tagName.equals("a")) { String url = tag.get("href").value; Tree addee = new Tree<>(); addee.key = "link"; addee.value = toText(tag); addee.add(new Tree<>("url", url)); returnee.add(addee); } else if (tagName.equals("span")) { Tree addee = new Tree<>(); addee.key = "text"; addee.value = toText(tag); returnee.add(addee); } else if (tagName.equals("br")) { returnee.add(new Tree<>("text", "\n")); } else { for (Tree child: children) { if (!child.key.equals("tag")) { returnee.add(child); continue; } child = turnIntoCommands(child); for (Tree command: child) { returnee.add(command); } } if (tagName.equals("p")) { returnee.add(new Tree<>("text", "\n")); returnee.add(new Tree<>("text", "\n")); } } return returnee; } // ---%-@-%--- public static class Attribute extends AttributedCharacterIterator.Attribute { public static final Attribute IMAGE = new Attribute("IMAGE"), ALT = new Attribute("ALT"), LINK = new Attribute("LINK"), BASICLATIN = new Attribute("BASICLATIN"), Y = new Attribute("Y"), OFFSCREEN = new Attribute("OFFSCREEN"), WHITESPACE = new Attribute("WHITESPACE"); public static final int COUNT = 7; // -=%=- private Attribute(String name) { super(name); } } // ---%-@-%--- RichTextPane2() { this.addComponentListener(this); text = new AttributedString(""); } }