This significantly improves painting performance for the Swing text components, especially styled text and HTML: - Many allocations of Rectangle and Segment objects are avoided now. - The BoxView (THE cental view class) now implements a sophisticated algoritm for painting its children. It first searches (binary) for a child that lies inside the clip, and then paints from this child in both directions until it is outside the clip. - The GlyphView moves some painting code from the GlyphPainter to the GlyphView itself. The GlyphPainter really only paints the actual glyphs.
2006-11-19 Roman Kennke <[EMAIL PROTECTED]> * javax/swing/text/BoxView.java (clipRect): New field. (tmpRect): New field. (layout): Reorganized code. Now uses layoutAxis() helper method. (layoutAxis): New helper method. (paint): Optimized by using cached Rectangle objects and a binary search for child views inside the clip. * javax/swing/text/CompositeView.java (insideAllocation): Made private and initialized in constructor. (getInsideAllocation): Removed initialization block for insideAllocation field. Avoid unnecessary allocations. * javax/swing/text/GlyphView.java (DefaultGlyphPainter.paint): Only paint the actual glyphs here The remaining stuff (background, underline and striking) is done in the GlpyhView itself. Avoid unnecessary allocations. (cached): A cached Segment instance. (getText): Return cached segment. (paint): Paint underline, strike and background here. Avoid unecessary allocs. /Roman
Index: javax/swing/text/BoxView.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/text/BoxView.java,v retrieving revision 1.25 diff -u -1 -5 -r1.25 BoxView.java --- javax/swing/text/BoxView.java 31 Aug 2006 21:07:05 -0000 1.25 +++ javax/swing/text/BoxView.java 19 Nov 2006 18:53:28 -0000 @@ -270,58 +270,132 @@ { int newLength = Math.max(2 * oldArray.length, numChildren + delta); newArray = new int[newLength]; System.arraycopy(oldArray, 0, newArray, 0, offset); System.arraycopy(oldArray, src, newArray, dst, numMove); } else { newArray = oldArray; System.arraycopy(newArray, src, newArray, dst, numMove); } return newArray; } /** + * A Rectangle instance to be reused in the paint() method below. + */ + private final Rectangle tmpRect = new Rectangle(); + + private Rectangle clipRect = new Rectangle(); + + /** * Renders the <code>Element</code> that is associated with this * <code>View</code>. * * @param g the <code>Graphics</code> context to render to * @param a the allocated region for the <code>Element</code> */ public void paint(Graphics g, Shape a) { - Rectangle alloc; - if (a instanceof Rectangle) - alloc = (Rectangle) a; - else - alloc = a.getBounds(); + // Try to avoid allocation if possible (almost all cases). + Rectangle alloc = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); - int x = alloc.x + getLeftInset(); - int y = alloc.y + getTopInset(); + // This returns a cached instance. + alloc = getInsideAllocation(alloc); - Rectangle clip = g.getClipBounds(); - Rectangle tmp = new Rectangle(); - int count = getViewCount(); - for (int i = 0; i < count; ++i) + // The following algorithm optimizes painting of a BoxView by taking + // advantage of the layout order of the box children. + // + // 1. It first searches a child that which's allocation is inside the clip. + // This is accomplished by an efficient binary search. This assumes + // that the children of the BoxView are laid out in the same order + // as their index within the view. This is true for the BoxView, but + // might not be the case for all subclasses. + // 2. Starting from the found view, it paints the children in both + // directions until the first view is hit that is outside the clip. + + // First we search a child view that is inside the clip. + + // Fetch the clip rect and calculate the center point of it. + clipRect = g.getClipBounds(clipRect); + int cX = clipRect.x + clipRect.width / 2; + int cY = clipRect.y + clipRect.height / 2; + + int viewCount = getViewCount(); + int up = viewCount; + int low = 0; + int mid = (up - low) / 2; + View start = getView(mid); + + int newMid; + // Use another cached instance here to avoid allocations during + // painting. + tmpRect.setBounds(alloc); + // This modifies tmpRect. + childAllocation(mid, tmpRect); + while (! clipRect.intersects(tmpRect)) + { + if (isBefore(cX, cY, tmpRect)) + { + up = mid; + newMid = (up - low) / 2 + low; + mid = (newMid == mid) ? newMid - 1 : newMid; + } + else + { + low = mid; + newMid = (up - low) / 2 + low; + mid = (newMid == mid) ? newMid + 1 : newMid; + } + if (mid >= 0 && mid < viewCount) + { + start = getView(mid); + tmpRect.setBounds(alloc); + childAllocation(mid, tmpRect); + } + else + break; + } + + if (mid >= 0 && mid < viewCount) { - tmp.x = x + getOffset(X_AXIS, i); - tmp.y = y + getOffset(Y_AXIS, i); - tmp.width = getSpan(X_AXIS, i); - tmp.height = getSpan(Y_AXIS, i); - if (tmp.intersects(clip)) - paintChild(g, tmp, i); + // Ok, we found one view that is inside the clip rect. Now paint the + // children before it that are inside the clip. + boolean inClip = true; + for (int i = mid - 1; i >= 0 && inClip; i--) + { + start = getView(i); + tmpRect.setBounds(alloc); + childAllocation(i, tmpRect); + inClip = clipRect.intersects(tmpRect); + if (inClip) + paintChild(g, tmpRect, i); + } + + // Now paint the found view and all views after it that lie inside the + // clip. + inClip = true; + for (int i = mid; i < viewCount && inClip; i++) + { + start = getView(i); + tmpRect.setBounds(alloc); + childAllocation(i, tmpRect); + inClip = clipRect.intersects(tmpRect); + if (inClip) + paintChild(g, tmpRect, i); + } } } /** * Returns the preferred span of the content managed by this * <code>View</code> along the specified <code>axis</code>. * * @param axis the axis * * @return the preferred span of this <code>View</code>. */ public float getPreferredSpan(int axis) { updateRequirements(axis); // Add margin. @@ -730,73 +804,56 @@ a.width = spans[X_AXIS][index]; a.height = spans[Y_AXIS][index]; } /** * Lays out the children of this <code>BoxView</code> with the specified * bounds. * * @param width the width of the allocated region for the children (that * is the inner allocation of this <code>BoxView</code> * @param height the height of the allocated region for the children (that * is the inner allocation of this <code>BoxView</code> */ protected void layout(int width, int height) { - int[] newSpan = new int[]{ width, height }; - int count = getViewCount(); - - // Update minor axis as appropriate. We need to first update the minor - // axis layout because that might affect the children's preferences along - // the major axis. - int minorAxis = myAxis == X_AXIS ? Y_AXIS : X_AXIS; - if ((! isLayoutValid(minorAxis)) || newSpan[minorAxis] != span[minorAxis]) - { - layoutValid[minorAxis] = false; - span[minorAxis] = newSpan[minorAxis]; - layoutMinorAxis(span[minorAxis], minorAxis, offsets[minorAxis], - spans[minorAxis]); - - // Update the child view's sizes. - for (int i = 0; i < count; ++i) - { - getView(i).setSize(spans[X_AXIS][i], spans[Y_AXIS][i]); - } - layoutValid[minorAxis] = true; - } - + layoutAxis(X_AXIS, width); + layoutAxis(Y_AXIS, height); + } - // Update major axis as appropriate. - if ((! isLayoutValid(myAxis)) || newSpan[myAxis] != span[myAxis]) + private void layoutAxis(int axis, int s) + { + if (span[axis] != s) + layoutValid[axis] = false; + if (! layoutValid[axis]) { - layoutValid[myAxis] = false; - span[myAxis] = newSpan[myAxis]; - layoutMajorAxis(span[myAxis], myAxis, offsets[myAxis], - spans[myAxis]); + span[axis] = s; + updateRequirements(axis); + if (axis == myAxis) + layoutMajorAxis(span[axis], axis, offsets[axis], spans[axis]); + else + layoutMinorAxis(span[axis], axis, offsets[axis], spans[axis]); + layoutValid[axis] = true; - // Update the child view's sizes. - for (int i = 0; i < count; ++i) + // Push out child layout. + int viewCount = getViewCount(); + for (int i = 0; i < viewCount; i++) { - getView(i).setSize(spans[X_AXIS][i], spans[Y_AXIS][i]); + View v = getView(i); + v.setSize(spans[X_AXIS][i], spans[Y_AXIS][i]); } - layoutValid[myAxis] = true; } - - if (layoutValid[myAxis] == false) - System.err.println("WARNING: Major axis layout must be valid after layout"); - if (layoutValid[minorAxis] == false) - System.err.println("Minor axis layout must be valid after layout"); } /** * Performs the layout along the major axis of a <code>BoxView</code>. * * @param targetSpan the (inner) span of the <code>BoxView</code> in which * to layout the children * @param axis the axis along which the layout is performed * @param offsets the array that holds the offsets of the children on exit * @param spans the array that holds the spans of the children on exit */ protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) { // Set the spans to the preferred sizes. Determine the space Index: javax/swing/text/CompositeView.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/text/CompositeView.java,v retrieving revision 1.23 diff -u -1 -5 -r1.23 CompositeView.java --- javax/swing/text/CompositeView.java 11 Nov 2006 11:02:07 -0000 1.23 +++ javax/swing/text/CompositeView.java 19 Nov 2006 18:53:28 -0000 @@ -56,31 +56,31 @@ /** * The child views of this <code>CompositeView</code>. */ private View[] children; /** * The number of child views. */ private int numChildren; /** * The allocation of this <code>View</code> minus its insets. This is * initialized in [EMAIL PROTECTED] #getInsideAllocation} and reused and modified in * [EMAIL PROTECTED] #childAllocation(int, Rectangle)}. */ - Rectangle insideAllocation; + private final Rectangle insideAllocation = new Rectangle(); /** * The insets of this <code>CompositeView</code>. This is initialized * in [EMAIL PROTECTED] #setInsets}. */ private short top; private short bottom; private short left; private short right; /** * Creates a new <code>CompositeView</code> for the given * <code>Element</code>. * * @param element the element that is rendered by this CompositeView @@ -515,44 +515,37 @@ * Also this translates from an immutable allocation to a mutable allocation * that is typically reused and further narrowed, like in * [EMAIL PROTECTED] #childAllocation}. * * @param a the allocation given to this <code>CompositeView</code> * * @return the allocation that is given to this <code>CompositeView</code> * minus this <code>CompositeView</code>'s insets or * <code>null</code> if a was <code>null</code> */ protected Rectangle getInsideAllocation(Shape a) { if (a == null) return null; - Rectangle alloc = a.getBounds(); + // Try to avoid allocation of Rectangle here. + Rectangle alloc = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); + // Initialize the inside allocation rectangle. This is done inside // a synchronized block in order to avoid multiple threads creating // this instance simultanously. - Rectangle inside; - synchronized(this) - { - inside = insideAllocation; - if (inside == null) - { - inside = new Rectangle(); - insideAllocation = inside; - } - } + Rectangle inside = insideAllocation; inside.x = alloc.x + left; inside.y = alloc.y + top; inside.width = alloc.width - left - right; inside.height = alloc.height - top - bottom; return inside; } /** * Sets the insets defined by attributes in <code>attributes</code>. This * queries the attribute keys [EMAIL PROTECTED] StyleConstants#SpaceAbove}, * [EMAIL PROTECTED] StyleConstants#SpaceBelow}, [EMAIL PROTECTED] StyleConstants#LeftIndent} and * [EMAIL PROTECTED] StyleConstants#RightIndent} and calls [EMAIL PROTECTED] #setInsets} to * actually set the insets on this <code>CompositeView</code>. * * @param attributes the attributes from which to query the insets Index: javax/swing/text/GlyphView.java =================================================================== RCS file: /cvsroot/classpath/classpath/javax/swing/text/GlyphView.java,v retrieving revision 1.25 diff -u -1 -5 -r1.25 GlyphView.java --- javax/swing/text/GlyphView.java 16 Nov 2006 13:03:04 -0000 1.25 +++ javax/swing/text/GlyphView.java 19 Nov 2006 18:53:29 -0000 @@ -266,68 +266,51 @@ return height; } /** * Paints the glyphs. * * @param view the glyph view to paint * @param g the graphics context to use for painting * @param a the allocation of the glyph view * @param p0 the start position (in the model) from which to paint * @param p1 the end position (in the model) to which to paint */ public void paint(GlyphView view, Graphics g, Shape a, int p0, int p1) { - Color oldColor = g.getColor(); - int height = (int) getHeight(view); + updateFontMetrics(view); + Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); + TabExpander tabEx = view.getTabExpander(); Segment txt = view.getText(p0, p1); - Rectangle bounds = a.getBounds(); - TabExpander tabEx = null; - View parent = view.getParent(); - if (parent instanceof TabExpander) - tabEx = (TabExpander) parent; - - int width = Utilities.getTabbedTextWidth(txt, g.getFontMetrics(), - bounds.x, tabEx, txt.offset); - // Fill the background of the text run. - Color background = view.getBackground(); - if (background != null) - { - g.setColor(background); - g.fillRect(bounds.x, bounds.y, width, height); - } - // Draw the actual text. - g.setColor(view.getForeground()); - g.setFont(view.getFont()); - int ascent = g.getFontMetrics().getAscent(); - Utilities.drawTabbedText(txt, bounds.x, bounds.y + ascent, g, tabEx, - txt.offset); - if (view.isStrikeThrough()) + // Find out the X location at which we have to paint. + int x = r.x; + int p = view.getStartOffset(); + if (p != p0) { - int strikeHeight = (int) (getAscent(view) / 2); - g.drawLine(bounds.x, bounds.y + strikeHeight, bounds.x + width, - bounds.y + strikeHeight); + int width = Utilities.getTabbedTextWidth(txt, fontMetrics,x, tabEx, + p); + x += width; } - if (view.isUnderline()) - { - int lineHeight = (int) getAscent(view); - g.drawLine(bounds.x, bounds.y + lineHeight, bounds.x + width, - bounds.y + lineHeight); - } - g.setColor(oldColor); + // Find out Y location. + int y = r.y + fontMetrics.getHeight() - fontMetrics.getDescent(); + + // Render the thing. + g.setFont(fontMetrics.getFont()); + Utilities.drawTabbedText(txt, x, y, g, tabEx, p0); + } /** * Maps a position in the document into the coordinate space of the View. * The output rectangle usually reflects the font height but has a width * of zero. * * @param view the glyph view * @param pos the position of the character in the model * @param a the area that is occupied by the view * @param b either [EMAIL PROTECTED] Position.Bias#Forward} or * [EMAIL PROTECTED] Position.Bias#Backward} depending on the preferred * direction bias. If <code>null</code> this defaults to * <code>Position.Bias.Forward</code> * @@ -553,44 +536,100 @@ } /** * Renders the <code>Element</code> that is associated with this * <code>View</code>. * * @param g the <code>Graphics</code> context to render to * @param a the allocated region for the <code>Element</code> */ public void paint(Graphics g, Shape a) { checkPainter(); int p0 = getStartOffset(); int p1 = getEndOffset(); + Rectangle r = a instanceof Rectangle ? (Rectangle) a : a.getBounds(); Container c = getContainer(); - // Paint layered highlights if there are any. + + Color fg = getForeground(); + JTextComponent tc = null; if (c instanceof JTextComponent) { - JTextComponent tc = (JTextComponent) c; + tc = (JTextComponent) c; + if (! tc.isEnabled()) + fg = tc.getDisabledTextColor(); + } + Color bg = getBackground(); + if (bg != null) + { + g.setColor(bg); + System.err.println("fill background: " + bg); + g.fillRect(r.x, r.y, r.width, r.height); + } + + + // Paint layered highlights if there are any. + if (tc != null) + { Highlighter h = tc.getHighlighter(); if (h instanceof LayeredHighlighter) { LayeredHighlighter lh = (LayeredHighlighter) h; lh.paintLayeredHighlights(g, p0, p1, a, tc, this); } } - getGlyphPainter().paint(this, g, a, p0, p1); + g.setColor(fg); + glyphPainter.paint(this, g, a, p0, p1); + boolean underline = isUnderline(); + boolean striked = isStrikeThrough(); + if (underline || striked) + { + View parent = getParent(); + // X coordinate. + if (parent != null && parent.getEndOffset() == p1) + { + // Strip whitespace. + Segment s = getText(p0, p1); + while (s.count > 0 && Character.isWhitespace(s.array[s.count - 1])) + { + p1--; + s.count--; + } + } + int x0 = r.x; + int p = getStartOffset(); + TabExpander tabEx = getTabExpander(); + if (p != p0) + x0 += (int) glyphPainter.getSpan(this, p, p0, tabEx, x0); + int x1 = x0 + (int) glyphPainter.getSpan(this, p0, p1, tabEx, x0); + // Y coordinate. + int y = r.y + r.height - (int) glyphPainter.getDescent(this); + if (underline) + { + int yTmp = y; + yTmp += 1; + g.drawLine(x0, yTmp, x1, yTmp); + } + if (striked) + { + int yTmp = y; + yTmp -= (int) glyphPainter.getAscent(this); + g.drawLine(x0, yTmp, x1, yTmp); + } + } } /** * Returns the preferred span of the content managed by this * <code>View</code> along the specified <code>axis</code>. * * @param axis the axis * * @return the preferred span of this <code>View</code>. */ public float getPreferredSpan(int axis) { float span = 0; checkPainter(); @@ -731,55 +770,56 @@ * * @return the end offset in the document model of the portion * of text that this view is responsible for */ public int getEndOffset() { Element el = getElement(); int offs; if (length > 0) offs = el.getStartOffset() + offset + length; else offs = el.getEndOffset(); return offs; } + private Segment cached = new Segment(); + /** * Returns the text segment that this view is responsible for. * * @param p0 the start index in the document model * @param p1 the end index in the document model * * @return the text segment that this view is responsible for */ public Segment getText(int p0, int p1) { - Segment txt = new Segment(); try { - getDocument().getText(p0, p1 - p0, txt); + getDocument().getText(p0, p1 - p0, cached); } catch (BadLocationException ex) { AssertionError ae; ae = new AssertionError("BadLocationException should not be " + "thrown here. p0 = " + p0 + ", p1 = " + p1); ae.initCause(ex); throw ae; } - return txt; + return cached; } /** * Returns the font for the text run for which this <code>GlyphView</code> * is responsible. * * @return the font for the text run for which this <code>GlyphView</code> * is responsible */ public Font getFont() { Document doc = getDocument(); Font font = null; if (doc instanceof StyledDocument) {