blob: 15d1006af0666157e21a38707c1658b9c9421094 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2020 Ericsson
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License 2.0 which
* accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tracecompass.internal.tmf.ui.widgets.timegraph;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.tracecompass.tmf.core.presentation.RGBAColor;
import org.eclipse.tracecompass.tmf.ui.colors.RGBAUtil;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.ITimeGraphPresentationProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.Utils;
/**
* Deferred draw util, can be used to store draws in a list so that they can be
* displayed at a later time.
*
* @author Matthew Khouzam
*/
@NonNullByDefault
public final class TimeGraphRender {
private interface IDrawable {
/**
* Draw the element
*
* @param provider
* the presentation provider for post-drawing
* @param gc
* the Graphics context
*/
public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc);
}
/**
* Deferred entry, basically a row to draw
*/
public static class DeferredEntry implements IDrawable {
private Rectangle fBounds;
private ITimeGraphEntry fEntry;
private final List<DeferredItem> fItems = new ArrayList<>();
/**
* Constructor
*
* @param entry
* the entry to draw
* @param bounds
* the bounds of the entry
*/
public DeferredEntry(ITimeGraphEntry entry, Rectangle bounds) {
fEntry = entry;
fBounds = bounds;
}
@Override
public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
/* Set the font for this item */
int height = fBounds.height;
setFontForHeight(height - getMarginForHeight(height), gc);
for (DeferredItem item : getItems()) {
item.draw(provider, gc);
}
if (provider != null) {
provider.postDrawEntry(fEntry, fBounds, gc);
}
}
/**
* gets the items list
*
* @return the items
*/
public List<DeferredItem> getItems() {
return fItems;
}
}
/**
* The deferredItem's responsibility is to set and reset the graphics
* context so that derived classes can draw in it.
*/
public abstract static class DeferredItem implements IDrawable {
/**
* Style with no border
*/
public static final int NO_BORDER = Integer.MIN_VALUE;
private final Collection<PostDrawEvent> fPDEs = new ArrayList<>();
private final Rectangle fBounds;
private RGBAColor fBgColor;
private final RGBAColor fBorderColor;
private final int fLineWidth;
/**
* Copy constructor
*
* @param other
* other {@link DeferredItem} to copy
*/
public DeferredItem(DeferredItem other) {
Rectangle bounds = other.fBounds;
fBounds = new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height);
fBgColor = other.getBackgroundColor();
fPDEs.addAll(other.getPostDrawEvents());
fBorderColor = other.fBorderColor;
fLineWidth = other.fLineWidth;
}
/**
* Constructor
*
* @param bounds
* bounding box of item
* @param backgroundColor
* the background color
* @param borderColor
* the border color
* @param lineWidth
* border width, NO_BORDER if it should not be drawn
*/
public DeferredItem(Rectangle bounds, RGBAColor backgroundColor, RGBAColor borderColor, int lineWidth) {
fBounds = bounds;
fLineWidth = lineWidth;
fBgColor = backgroundColor;
fBorderColor = borderColor;
}
/**
* Add a post draw event
*
* @param pde
* post draw event
*/
public void add(PostDrawEvent pde) {
fPDEs.add(pde);
}
@Override
public final void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
Rectangle bounds = getBounds();
// basic sanity
if (bounds.width <= 0 || bounds.height <= 0) {
return;
}
int prevAlpha = gc.getAlpha();
Color prevBgColor = gc.getBackground();
Color prevFgColor = gc.getForeground();
int prevLineWidth = gc.getLineWidth();
setContext(gc);
innerDraw(gc);
if (fLineWidth != NO_BORDER) {
setContext(gc);
gc.setAlpha(fBorderColor.getAlpha());
drawBorder(gc);
}
setContext(gc);
drawLabel(gc);
if (provider != null) {
gc.setLineWidth(prevLineWidth);
gc.setBackground(prevBgColor);
gc.setForeground(prevFgColor);
gc.setAlpha(prevAlpha);
postDraw(provider, gc);
}
// reset context
gc.setLineWidth(prevLineWidth);
gc.setBackground(prevBgColor);
gc.setForeground(prevFgColor);
gc.setAlpha(prevAlpha);
}
/**
* This function has the context set for it. The most important being
* ForegroundColor
*
* @param gc
* the graphics context
*/
protected void drawBorder(GC gc) {
gc.drawRectangle(getBounds());
}
/**
* Draw the label if applicable
*
* @param gc
* the graphics context
*/
protected void drawLabel(GC gc) {
// do nothing
}
/**
* Gets the background color
*
* @return the background color
*/
public RGBAColor getBackgroundColor() {
return fBgColor;
}
/**
* Gets the border color
*
* @return the border color
*/
public @Nullable RGBAColor getBorderColor() {
return fBorderColor;
}
/**
* Gets the bounds (bounding box)
*
* @return the bounds
*/
public Rectangle getBounds() {
return fBounds;
}
/**
* Gets the post draw events
*
* @return the post draw events
*/
private Collection<PostDrawEvent> getPostDrawEvents() {
return fPDEs;
}
/**
* Draw where the line width, background and foreground color and alpha
* is already set and reset.
*
* @param gc
* the Graphics context
*/
protected abstract void innerDraw(GC gc);
private final void postDraw(ITimeGraphPresentationProvider provider, GC gc) {
for (PostDrawEvent pde : fPDEs) {
pde.draw(provider, gc);
}
}
/**
* Sets the background color, useful for merging states
*
* @param color
* the background color
*/
protected void setBackgroundColor(RGBAColor color) {
fBgColor = color;
}
/**
* Sets the graphical context.
* <ul>
* <li>background color</li>
* <li>alpha</li>
* <li>foreground color</li>
* <li>line width</li>
* </ul>
*
* @param gc
* the graphics context
*/
private void setContext(GC gc) {
RGBAColor backgroundRgba = getBackgroundColor();
Color bgColor = getColor(backgroundRgba.toInt());
gc.setAlpha(backgroundRgba.getAlpha());
gc.setBackground(bgColor);
RGBAColor foregroundRgba = getBorderColor();
if (foregroundRgba == null) {
foregroundRgba = BLACK;
}
Color fgColor = getColor(foregroundRgba.toInt());
gc.setForeground(fgColor);
if (fLineWidth >= 0) {
gc.setLineWidth(fLineWidth);
}
}
}
/**
* Deferred poly line drawing
*/
public static class DeferredLine implements IDrawable {
private Rectangle fBounds;
private long fMin;
private List<List<LongPoint>> fSeriesPoints;
private RGBAColor fColorRGBA;
private double fScale;
/**
* Constructor
*
* @param bounds
* bounds of the line
* @param min
* minimum value of the line
* @param seriesPoints
* list of series (list of long points)
* @param colorRGBA
* the color of the line
* @param scale
* the scale of the line
*/
public DeferredLine(Rectangle bounds, long min, List<List<LongPoint>> seriesPoints, RGBAColor colorRGBA, double scale) {
fBounds = bounds;
fMin = min;
fSeriesPoints = seriesPoints;
fColorRGBA = colorRGBA;
fScale = scale;
}
@Override
public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
RGBAColor rgba = fColorRGBA;
int colorInt = rgba.toInt();
Color color = getColor(colorInt);
for (int i = 0; i < this.fSeriesPoints.size(); i++) {
Color prev = gc.getForeground();
int prevAlpha = gc.getAlpha();
gc.setAlpha(rgba.getAlpha());
gc.setForeground(color);
List<LongPoint> series = fSeriesPoints.get(i);
int[] points = new int[series.size() * 2];
for (int point = 0; point < series.size(); point++) {
LongPoint longPoint = series.get(point);
points[point * 2] = longPoint.x;
points[point * 2 + 1] = fBounds.height - (int) ((longPoint.y - fMin) * fScale) + fBounds.y;
}
gc.drawPolyline(points);
gc.setForeground(prev);
gc.setAlpha(prevAlpha);
}
}
}
/**
* Deferred Transparent State
*/
public static class DeferredTransparentState extends DeferredItem {
/**
* Constructor
*
* @param bounds
* the bounds
* @param bgColor
* the background color
*/
public DeferredTransparentState(Rectangle bounds, RGBAColor bgColor) {
super(bounds, bgColor, BLACK, DeferredItem.NO_BORDER);
}
@Override
protected void innerDraw(GC gc) {
Rectangle drawRect = getBounds();
if (drawRect.width >= 2) {
gc.fillRectangle(drawRect);
if (drawRect.width > 2) {
// Draw the top and bottom borders
RGBAColor foregroundRGB = BLACK;
gc.setAlpha(foregroundRGB.getAlpha());
gc.setForeground(getColor(foregroundRGB.toInt()));
gc.drawLine(drawRect.x, drawRect.y, drawRect.x + drawRect.width - 1, drawRect.y);
gc.drawLine(drawRect.x, drawRect.y + drawRect.height - 1, drawRect.x + drawRect.width - 1, drawRect.y + drawRect.height - 1);
}
} else {
gc.setForeground(gc.getBackground());
gc.drawLine(drawRect.x, drawRect.y, drawRect.x, drawRect.y + drawRect.height - 1);
}
}
}
/**
* Deferred segment, can be a collection of points or a single point. Starts
* as one point then grows on the X axis
*/
public static class DeferredSegment implements IDrawable {
private final int fX;
private final int fY;
private int fLength;
/**
* Constructor
*
* @param x
* x coordinate
* @param y
* y coordinate
*/
public DeferredSegment(int x, int y) {
fX = x;
fY = y;
fLength = 1;
}
/**
* Does the segment contain another point?
*
* @param x
* the x coordinate
* @param y
* the Y coordinate
* @return true if the segment crosses that point, false otherwise
*/
public boolean contains(int x, int y) {
return (y == fY) && x >= fX && x <= fX + fLength;
}
@Override
public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
if (fLength == 1) {
gc.drawPoint(fX, fY);
} else {
gc.drawLine(fX, fY, fX + fLength - 1, fY);
}
}
/**
* Extend the point if it's at the end
*
* @param x
* the x coordinate to extend to
*/
public void extend(int x) {
if (x == fX + fLength) {
fLength++;
}
}
}
/**
* Deferred State class, the bigger states that can have labels and corners.
* (in the world of graphics, these are LOD1 and tiny are LOD0)
*/
public static class DeferredState extends DeferredItem {
private final int fArc;
private final @Nullable String fLabel;
/**
* Constructor
*
* @param bounds
* the bounds of the item
* @param bgColor
* the background color
* @param borderColor
* the border color
* @param arc
* the radius of the arc
* @param lineWidth
* the border width
* @param label
* the label to display, can be {@code null}
*/
public DeferredState(Rectangle bounds, RGBAColor bgColor, RGBAColor borderColor, int arc, int lineWidth, @Nullable String label) {
super(bounds, bgColor, borderColor, lineWidth);
fArc = arc;
fLabel = label;
}
@Override
protected void drawBorder(GC gc) {
Rectangle bounds = getBounds();
gc.drawRoundRectangle(bounds.x, bounds.y, bounds.width, bounds.height, fArc, fArc);
}
@Override
protected void drawLabel(GC gc) {
Rectangle bounds = getBounds();
RGBAColor backgroundColor = getBackgroundColor();
if (fLabel != null && !fLabel.isEmpty() && bounds.width > bounds.height) {
gc.setForeground(Utils.getDistinctColor(RGBAUtil.fromRGBAColor(backgroundColor).rgb));
Utils.drawText(gc, fLabel, bounds.x, bounds.y, bounds.width, bounds.height, true, true);
}
}
@Override
protected void innerDraw(GC gc) {
Rectangle bounds = getBounds();
gc.fillRoundRectangle(bounds.x, bounds.y, bounds.width, bounds.height, fArc, fArc);
}
}
/**
* States that are smaller than a pixel
*/
public static class DeferredTinyState extends DeferredItem {
/**
* Blend 2 colors
*
* @param alpha
* the ratio
* @param c0
* color channel 1, the base
* @param c1
* color channel 2, the top layer
* @return the blended color
*/
private static int alphaBlend(int alpha, short c0, short c1) {
return (int) (c0 * ((255 - alpha) / 255.0) + c1 * ((alpha) / 255.0));
}
/**
* Can the items be merged? (note, right needs to have x >= left's x)
*
* @param left
* the left element
* @param right
* the right element
* @return true if the items can be merged
*/
public static boolean areMergeable(DeferredItem left, DeferredItem right) {
Rectangle rightBounds = right.getBounds();
Rectangle largerBounds = new Rectangle(rightBounds.x - 1, rightBounds.y, rightBounds.width + 1, rightBounds.height);
return Objects.equals(left.getBackgroundColor(), right.getBackgroundColor())
&& Objects.equals(left.getBorderColor(), right.getBorderColor())
&& largerBounds.intersects(left.getBounds())
&& largerBounds.height == right.getBounds().height;
}
/**
* Copy constructor
*
* @param other
* the other state
*/
public DeferredTinyState(DeferredTinyState other) {
super(other);
}
/**
* Constructor
*
* @param bounds
* the bounds of the state
* @param backgroundColor
* the background color
* @param borderColor
* the border color
* @param lineWidth
* the line width, could be NO_BORDER
*/
public DeferredTinyState(Rectangle bounds, RGBAColor backgroundColor, RGBAColor borderColor, int lineWidth) {
super(bounds, backgroundColor, borderColor, lineWidth);
}
/**
* Extend a tiny state if another is overlapping
*
* @param other
* the other state to overlap
* @return if the size was grown.
*/
public boolean extend(DeferredTinyState other) {
Rectangle bounds = getBounds();
if (areMergeable(this, other)) {
bounds.add(other.getBounds());
return true;
}
return false;
}
@Override
protected void innerDraw(GC gc) {
gc.fillRectangle(getBounds());
}
/**
* Squash two pixels, assume the height is the largest.
*
* @param other
* the state to squash.
* @return true if the state is squashed
*/
public boolean squash(DeferredTinyState other) {
Rectangle bounds = getBounds();
Rectangle otherBounds = other.getBounds();
if (bounds.x != otherBounds.x || bounds.width != otherBounds.width) {
return false;
}
// if the color or the height change here, areMergeable will no
// longer hold true.
RGBAColor prevColor = getBackgroundColor();
RGBAColor newColor = other.getBackgroundColor();
bounds.y = Math.min(bounds.y, otherBounds.y);
bounds.height = Math.max(bounds.height, otherBounds.height);
int alpha = newColor.getAlpha() / 2;
setBackgroundColor(new RGBAColor(alphaBlend(alpha, prevColor.getRed(), newColor.getRed()),
alphaBlend(alpha, prevColor.getGreen(), newColor.getGreen()),
alphaBlend(alpha, prevColor.getBlue(), newColor.getBlue()),
alphaBlend(alpha, prevColor.getAlpha(), newColor.getAlpha())));
return true;
}
}
/**
* Long point, a point with a Y value that's long rather than an int.
*/
public static class LongPoint {
final int x;
final long y;
/**
* Constructor
*
* @param x
* x value
* @param y
* y value
*/
public LongPoint(int x, long y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof LongPoint) {
LongPoint longPoint = (LongPoint) obj;
return longPoint.x == x && longPoint.y == y;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
/**
* Post draw event
*/
public static class PostDrawEvent implements IDrawable {
private Rectangle fBounds;
private ITimeEvent fEvent;
/**
* Constructor
*
* @param event
* the event
* @param bounds
* the bounds of the event
*/
public PostDrawEvent(ITimeEvent event, Rectangle bounds) {
fEvent = event;
fBounds = bounds;
}
@Override
public void draw(@Nullable ITimeGraphPresentationProvider provider, GC gc) {
if (provider != null) {
provider.postDrawEvent(fEvent, fBounds, gc);
}
}
}
private static final RGBAColor BLACK = new RGBAColor(0, 0, 0, 255);
private static final ColorRegistry COLOR_REGISTRY = new ColorRegistry();
private static final Map<Integer, Font> FONTS = new HashMap<>();
/** Dots per inch */
private static final int DPI = 96;
/** points per inch */
private static final int PPI = 72;
/**
* Get the color for a given color integer
*
* @param colorInt
* the color integer (0xRRGGBBAA)
* @return the {@link Color}
*/
public static Color getColor(int colorInt) {
String hexRGB = Integer.toHexString((colorInt >> 8) & 0xffffff);
Color color = COLOR_REGISTRY.get(hexRGB);
if (color == null) {
COLOR_REGISTRY.put(hexRGB, RGBAUtil.fromInt(colorInt).rgb);
color = Objects.requireNonNull(COLOR_REGISTRY.get(hexRGB));
}
return color;
}
/**
* Sets the font height for a given height
*
* @param pixels
* the height in pixels
* @param gc
* the graphics context
*/
public static void setFontForHeight(int pixels, GC gc) {
/* convert font height from pixels to points */
int height = Math.max(pixels * PPI / DPI, 1);
Font font = FONTS.computeIfAbsent(height, fontHeight -> {
FontData fontData = gc.getFont().getFontData()[0];
fontData.setHeight(fontHeight);
return new Font(gc.getDevice(), fontData);
});
gc.setFont(font);
}
/**
* Get the margin for a height
*
* @param height
* the height
* @return the margin
*/
public static int getMarginForHeight(int height) {
/*
* State rectangle is smaller than the item bounds when height is > 4.
* Don't use any margin if the height is below or equal that threshold.
* Use a maximum of 6 pixels for both margins, otherwise try to use 13
* pixels for the state height, but with a minimum margin of 1.
*/
final int MARGIN_THRESHOLD = 4;
final int PREFERRED_HEIGHT = 13;
final int MIN_MARGIN = 1;
final int MAX_MARGIN = 6;
return height <= MARGIN_THRESHOLD ? 0 : Math.max(Math.min(height - PREFERRED_HEIGHT, MAX_MARGIN), MIN_MARGIN);
}
}