Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reposition labels for partially visible frames. See bug #33. #39

Merged
merged 2 commits into from
Mar 18, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import java.awt.*;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
Expand All @@ -28,6 +29,25 @@
* @see FlameGraph
*/
public class FlameGraphPainter<T> {

/**
* A short string to display in place of labels that are too long to fit the
* available space.
*/
private static final String LONG_TEXT_PLACEHOLDER = "...";

/**
* The font used to display frame labels
*/
private Font frameLabelFont;

/**
* If a frame is clipped, we'll shift the label to make it visible but show it with
* a modified (italicised by default) font to highlight that the frame is only partially
* visible.
*/
private Font frameLabelFontForPartialFrames;

public Color highlightedColor;
public Color frameGapColor;
public boolean frameGapEnabled = true;
Expand Down Expand Up @@ -60,11 +80,14 @@ public class FlameGraphPainter<T> {
private final int internalPadding = 2;
protected boolean paintDetails = true;


public FlameGraphPainter(List<FrameBox<T>> frames,
List<Function<T, String>> nodeToTextCandidates,
Function<T, String> rootFrameToText,
Function<T, Color> frameColorFunction) {

this.frameLabelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12);
this.frameLabelFontForPartialFrames = new Font(Font.SANS_SERIF, Font.ITALIC, 12);

this.frames = frames;
this.depth = this.frames.stream().mapToInt(fb -> fb.stackDepth).max().orElse(0);
visibleDepth = depth;
Expand All @@ -74,6 +97,22 @@ public FlameGraphPainter(List<FrameBox<T>> frames,
updateUI();
}

public Font getFrameLabelFont() {
return this.frameLabelFont;
}

/**
* Sets the font used to display frame labels. Internally an italicised version is also
* created for use in special cases.
*
* @param font the font ({@code null} not permitted).
*/
public void setFrameLabelFont(Font font) {
Objects.requireNonNull(font);
this.frameLabelFont = font;
this.frameLabelFontForPartialFrames = font.deriveFont(Font.ITALIC);
}

/**
* This method is used to resync colors when the LaF changes
*/
Expand All @@ -83,7 +122,7 @@ public void updateUI() {
}

private int getFrameBoxHeight(Graphics2D g2) {
return g2.getFontMetrics().getAscent() + (textBorder * 2) + frameGapWidth * 2;
return g2.getFontMetrics(this.frameLabelFont).getAscent() + (textBorder * 2) + frameGapWidth * 2;
}

public int computeFlameGraphMinimapHeight(int thumbnailWidth) {
Expand Down Expand Up @@ -137,7 +176,7 @@ public Dimension computeFlameGraphDimension(Graphics2D g2, Rectangle visibleRect
}

private float getFrameBoxTextOffset(Graphics2D g2) {
return getFrameBoxHeight(g2) - (g2.getFontMetrics().getDescent() / 2f) - textBorder - frameGapWidth;
return getFrameBoxHeight(g2) - (g2.getFontMetrics(frameLabelFont).getDescent() / 2f) - textBorder - frameGapWidth;
}

public void paint(Graphics2D g2, Rectangle visibleRect) {
Expand Down Expand Up @@ -167,9 +206,11 @@ private void paint(Graphics2D g2, Rectangle visibleRect, boolean minimapMode) {
rectOnCanvas.y = frameBoxHeight * rootFrame.stackDepth;
rectOnCanvas.height = frameBoxHeight;

if (visibleRect.intersects(rectOnCanvas)) {
Rectangle intersection = visibleRect.intersection(rectOnCanvas);
if (!intersection.isEmpty()) {
paintRootFrameRectangle(g2d, rectOnCanvas,
rootFrameToText.apply(rootFrame.actualNode),
intersection,
handleFocus(frameColorFunction.apply(rootFrame.actualNode),
hoveredFrame == rootFrame,
false,
Expand All @@ -194,9 +235,11 @@ private void paint(Graphics2D g2, Rectangle visibleRect, boolean minimapMode) {
rectOnCanvas.y = frameBoxHeight * frame.stackDepth;
rectOnCanvas.height = frameBoxHeight;

if (visibleRect.intersects(rectOnCanvas)) {
Rectangle intersection = visibleRect.intersection(rectOnCanvas);
if (!intersection.isEmpty()) {
paintNodeFrameRectangle(g2d, rectOnCanvas,
frame.actualNode,
intersection,
handleFocus(frameColorFunction.apply(frame.actualNode),
hoveredFrame == frame,
false,
Expand All @@ -219,7 +262,7 @@ private void paint(Graphics2D g2, Rectangle visibleRect, boolean minimapMode) {
var drawTimeMs = "FrameGraph width " + flameGraphWidth + " Zoom Factor " + zoomFactor + " Coordinate (" + visibleRect.x + ", " + visibleRect.y + ") size (" +
visibleRect.width + ", " + visibleRect.height +
") , Draw time: " + (System.currentTimeMillis() - start) + " ms";
var nowWidth = g2d.getFontMetrics().stringWidth(drawTimeMs);
var nowWidth = g2d.getFontMetrics(frameLabelFont).stringWidth(drawTimeMs);
g2d.setColor(Color.DARK_GRAY);
g2d.fillRect(visibleRect.x + visibleRect.width - nowWidth - textBorder * 2,
visibleRect.y + visibleRect.height - frameBoxHeight,
Expand Down Expand Up @@ -283,31 +326,63 @@ private Color handleFocus(Color bgColor, boolean hovered, boolean highlighted, b
return bgColor;
}

private void paintNodeFrameRectangle(Graphics2D g2, Rectangle frameRect, T node, Color bgColor, Color frameGapColor, boolean minimapMode) {
/**
* Paints a standard frame.
*
* @param g2 the graphics target.
* @param frameRect the frame rectangle (may fall partly outside the visible region).
* @param node the underlying node (used for label generation).
* @param intersection the intersection between the frame rectangle and the visible region
* (used to position text labels).
* @param bgColor the background color.
* @param frameGapColor the frame gap color.
* @param minimapMode is the minimap in the process of being rendered?
*/
private void paintNodeFrameRectangle(Graphics2D g2, Rectangle frameRect, T node, Rectangle intersection, Color bgColor, Color frameGapColor, boolean minimapMode) {
var frameRectSurface = paintFrameRectangle(g2, frameRect, bgColor, frameGapColor, minimapMode);
if (minimapMode) {
return;
}
// choose font depending on whether the left-side of the frame is clipped
final Font labelFont = (frameRect.x == intersection.x) ? frameLabelFont : frameLabelFontForPartialFrames;
paintFrameText(node,
g2,
frameRectSurface.width - textBorder * 2 - frameBorderWidth * 2,
labelFont,
intersection.width - textBorder * 2 - frameBorderWidth * 2,
text -> {
g2.setFont(labelFont);
g2.setColor(Colors.foregroundColor(bgColor));
g2.drawString(text, frameRect.x + textBorder, frameRect.y + getFrameBoxTextOffset(g2));
g2.drawString(text, intersection.x + textBorder, frameRect.y + getFrameBoxTextOffset(g2));
});
}

private void paintRootFrameRectangle(Graphics2D g2, Rectangle rect, String str, Color bgColor, Color gapColor, boolean minimapMode) {
/**
* Paints the root frame.
*
* @param g2 the graphics target.
* @param rect the frame region (may fall outside visible area).
* @param str the text to display.
* @param intersection the intersection between the frame rectangle and the visible region
(used to position the text label).
* @param bgColor the background color.
* @param gapColor the gap color.
* @param minimapMode is the minimap in the process of being rendered?
*/
private void paintRootFrameRectangle(Graphics2D g2, Rectangle rect, String str, Rectangle intersection, Color bgColor, Color gapColor, boolean minimapMode) {
var frameRectSurface = paintFrameRectangle(g2, rect, bgColor, gapColor, minimapMode);
if (minimapMode) {
return;
}
paintFrameText(str,
// choose a font depending on whether the left-side of the frame is clipped
final Font labelFont = (rect.x == intersection.x) ? frameLabelFont : frameLabelFontForPartialFrames;
paintRootFrameText(str,
g2,
frameRectSurface.width - textBorder * 2 - frameGapWidth * 2,
labelFont,
intersection.width - textBorder * 2 - frameGapWidth * 2,
text -> {
g2.setFont(labelFont);
g2.setColor(Colors.foregroundColor(bgColor));
g2.drawString(text, textBorder + frameBorderWidth, getFrameBoxTextOffset(g2));
g2.drawString(text, intersection.x + textBorder + frameBorderWidth, getFrameBoxTextOffset(g2));
});
}

Expand Down Expand Up @@ -383,8 +458,8 @@ public void stopHover() {
hoveredFrame = null;
}

private void paintFrameText(T node, Graphics2D g2, double targetWidth, Consumer<String> textConsumer) {
var metrics = g2.getFontMetrics();
private void paintFrameText(T node, Graphics2D g2, Font font, double targetWidth, Consumer<String> textConsumer) {
var metrics = g2.getFontMetrics(font);

nodeToTextCandidates.stream()
.map(f -> f.apply(node))
Expand All @@ -396,24 +471,24 @@ private void paintFrameText(T node, Graphics2D g2, double targetWidth, Consumer<
.ifPresentOrElse(
textConsumer,
() -> {
var textBounds = metrics.getStringBounds("...", g2);
var textBounds = metrics.getStringBounds(LONG_TEXT_PLACEHOLDER, g2);
if (!(textBounds.getWidth() > targetWidth)) {
textConsumer.accept("...");
textConsumer.accept(LONG_TEXT_PLACEHOLDER);
}
}
);
}

private static void paintFrameText(String text, Graphics2D g2, double targetWidth, Consumer<String> textConsumer) {
var metrics = g2.getFontMetrics();
private static void paintRootFrameText(String text, Graphics2D g2, Font font, double targetWidth, Consumer<String> textConsumer) {
var metrics = g2.getFontMetrics(font);

var textBounds = metrics.getStringBounds(text, g2);
if (!(textBounds.getWidth() > targetWidth)) {
textConsumer.accept(text);
}
textBounds = metrics.getStringBounds("...", g2);
textBounds = metrics.getStringBounds(LONG_TEXT_PLACEHOLDER, g2);
if (!(textBounds.getWidth() > targetWidth)) {
textConsumer.accept("...");
textConsumer.accept(LONG_TEXT_PLACEHOLDER);
}
// don't draw text
}
Expand Down