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)
       {

Reply via email to