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