diff --git a/fireplace-app/src/main/java/io/github/bric3/fireplace/DimmingFrameColorProvider.java b/fireplace-app/src/main/java/io/github/bric3/fireplace/DimmingFrameColorProvider.java new file mode 100644 index 00000000..7c22b0b3 --- /dev/null +++ b/fireplace-app/src/main/java/io/github/bric3/fireplace/DimmingFrameColorProvider.java @@ -0,0 +1,113 @@ +/* + * Copyright 2021 Datadog, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.github.bric3.fireplace; + +import io.github.bric3.fireplace.core.ui.Colors; +import io.github.bric3.fireplace.core.ui.DarkLightColor; +import io.github.bric3.fireplace.flamegraph.FrameBox; +import io.github.bric3.fireplace.flamegraph.FrameColorProvider; + +import java.awt.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isFocusedFrame; +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isFocusing; +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isHighlightedFrame; +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isHighlighting; +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isHovered; +import static io.github.bric3.fireplace.flamegraph.FrameRenderingFlags.isMinimapMode; + +class DimmingFrameColorProvider implements FrameColorProvider { + public static final Color DIMMED_TEXT = new DarkLightColor( + Colors.rgba(28, 43, 52, 0.68f), + Colors.rgba(255, 255, 255, 0.51f) + ); + + public static final Color ROOT_NODE = new DarkLightColor( + new Color(0xffeaf6fc), + new Color(0xff091222) + ); + private final Function, Color> baseColorFunction; + private final ColorModel reusableDataStructure = new ColorModel(null, null); + + private final ConcurrentHashMap dimmedColorCache = new ConcurrentHashMap<>(); + + public DimmingFrameColorProvider(Function, Color> baseColorFunction) { + this.baseColorFunction = baseColorFunction; + } + + + @Override + public ColorModel getColors(FrameBox frame, int flags) { + Color backgroundColor; + Color foreground; + + var rootNode = frame.isRoot(); + if (rootNode) { + backgroundColor = ROOT_NODE; + } else { + backgroundColor = baseColorFunction.apply(frame); + } + + if (!rootNode && shouldDim(flags) && !isMinimapMode(flags)) { + backgroundColor = cachedDim(backgroundColor); + foreground = DIMMED_TEXT; + } else { + foreground = Colors.foregroundColor(backgroundColor); + } + + if (isHovered(flags)) { + backgroundColor = Colors.blend(backgroundColor, Colors.translucent_black_40); + } + + return reusableDataStructure.set( + backgroundColor, + foreground + ); + } + + /** + * Dim only if not highlighted or not focused + * + * - highlighting and not highlighted => dim + * - focusing and not focused => dim + * - highlighting and focusing + * - highlighted => nope + * - focusing => nope + */ + private boolean shouldDim(int flags) { + var highlighting = isHighlighting(flags); + var highlightedFrame = isHighlightedFrame(flags); + var focusing = isFocusing(flags); + var focusedFrame = isFocusedFrame(flags); + + + var dimmedForHighlighting = highlighting && !highlightedFrame; + var dimmedForFocus = focusing && !focusedFrame; + + + return (dimmedForHighlighting || dimmedForFocus) + && !(highlighting + && focusing + && (highlightedFrame || focusedFrame)); + } + + private Color cachedDim(Color color) { + return dimmedColorCache.computeIfAbsent(color, Colors::dim); + } +} diff --git a/fireplace-app/src/main/java/io/github/bric3/fireplace/FlameGraphTab.java b/fireplace-app/src/main/java/io/github/bric3/fireplace/FlameGraphTab.java index 1557248e..8dfc1bca 100644 --- a/fireplace-app/src/main/java/io/github/bric3/fireplace/FlameGraphTab.java +++ b/fireplace-app/src/main/java/io/github/bric3/fireplace/FlameGraphTab.java @@ -11,8 +11,10 @@ import io.github.bric3.fireplace.core.ui.Colors; import io.github.bric3.fireplace.core.ui.Colors.Palette; +import io.github.bric3.fireplace.core.ui.DarkLightColor; import io.github.bric3.fireplace.flamegraph.ColorMapper; import io.github.bric3.fireplace.flamegraph.FlamegraphView; +import io.github.bric3.fireplace.flamegraph.FrameFontProvider; import io.github.bric3.fireplace.flamegraph.FrameTextsProvider; import io.github.bric3.fireplace.flamegraph.ZoomAnimation; import org.openjdk.jmc.common.util.FormatToolkit; @@ -47,7 +49,8 @@ public FlameGraphTab() { jfrFlamegraphView.configureCanvas(ToolTipManager.sharedInstance()::registerComponent); jfrFlamegraphView.putClientProperty(FlamegraphView.SHOW_STATS, true); // jfrFlameGraph.setTooltipComponentSupplier(BalloonToolTip::new); - jfrFlamegraphView.setMinimapShadeColorSupplier(() -> Colors.isDarkMode() ? Colors.translucent_black_40 : Colors.translucent_white_80); + var minimapShade = new DarkLightColor(Colors.translucent_white_80, Colors.translucent_black_40); + jfrFlamegraphView.setMinimapShadeColorSupplier(() -> minimapShade); var zoomAnimation = new ZoomAnimation(); zoomAnimation.install(jfrFlamegraphView); @@ -57,10 +60,10 @@ public FlameGraphTab() { colorModeJComboBox.setSelectedItem(defaultFrameColorMode); ActionListener updateColorSettingsListener = e -> { - jfrFlamegraphView.setColorFunction( - ((JfrFrameColorMode) colorModeJComboBox.getSelectedItem()) - .colorMapperUsing(ColorMapper.ofObjectHashUsing( - ((Palette) colorPaletteJComboBox.getSelectedItem()).colors()))); + var frameBoxColorFunction = ((JfrFrameColorMode) colorModeJComboBox.getSelectedItem()) + .colorMapperUsing(ColorMapper.ofObjectHashUsing( + ((Palette) colorPaletteJComboBox.getSelectedItem()).colors())); + jfrFlamegraphView.setFrameColorProvider(new DimmingFrameColorProvider<>(frameBoxColorFunction)); jfrFlamegraphView.requestRepaint(); }; colorPaletteJComboBox.addActionListener(updateColorSettingsListener); @@ -98,7 +101,7 @@ public FlameGraphTab() { jfrFlamegraphView.showMinimap(defaultShowMinimap); jfrFlamegraphView.configureCanvas(ToolTipManager.sharedInstance()::registerComponent); jfrFlamegraphView.putClientProperty(FlamegraphView.SHOW_STATS, true); - jfrFlamegraphView.setMinimapShadeColorSupplier(() -> Colors.isDarkMode() ? Colors.translucent_black_40 : Colors.translucent_white_80); + jfrFlamegraphView.setMinimapShadeColorSupplier(() -> minimapShade); zoomAnimation.install(jfrFlamegraphView); if (dataApplier != null) { dataApplier.accept(jfrFlamegraphView); @@ -175,12 +178,7 @@ public FlameGraphTab() { add(controlPanel, BorderLayout.NORTH); add(wrapper, BorderLayout.CENTER); } - - public FlameGraphTab(StacktraceTreeModel stacktraceTreeModel) { - this(); - setStacktraceTreeModel(stacktraceTreeModel); - } - + public void setStacktraceTreeModel(StacktraceTreeModel stacktraceTreeModel) { dataApplier = dataApplier(stacktraceTreeModel); dataApplier.accept(jfrFlamegraphView); @@ -205,7 +203,8 @@ private Consumer> dataApplier(StacktraceTreeModel stacktrac frame -> frame.isRoot() ? "" : FormatToolkit.getHumanReadable(frame.actualNode.getFrame().getMethod(), false, false, false, false, true, false), frame -> frame.isRoot() ? "" : frame.actualNode.getFrame().getMethod().getMethodName() ), - defaultFrameColorMode.colorMapperUsing(ColorMapper.ofObjectHashUsing(defaultColorPalette.colors())), + new DimmingFrameColorProvider(defaultFrameColorMode.colorMapperUsing(ColorMapper.ofObjectHashUsing(defaultColorPalette.colors()))), + FrameFontProvider.defaultFontProvider(), frame -> { if (frame.isRoot()) { return ""; diff --git a/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/GlassJPanel.java b/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/GlassJPanel.java index 2fc71ca0..0d159127 100644 --- a/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/GlassJPanel.java +++ b/fireplace-app/src/main/java/io/github/bric3/fireplace/ui/GlassJPanel.java @@ -1,11 +1,17 @@ package io.github.bric3.fireplace.ui; import io.github.bric3.fireplace.core.ui.Colors; +import io.github.bric3.fireplace.core.ui.DarkLightColor; import javax.swing.*; import java.awt.*; public class GlassJPanel extends JPanel { + private static final Color TRANSLUCENT_BACKGROUND = new DarkLightColor( + Colors.translucent_white_D0, + Colors.translucent_black_80 + ); + public GlassJPanel() { this(new GridBagLayout()); } @@ -17,7 +23,7 @@ public GlassJPanel(LayoutManager layout) { @Override protected void paintComponent(Graphics g) { var g2 = (Graphics2D) g; - g2.setColor(Colors.isDarkMode() ? Colors.translucent_black_80 : Colors.translucent_white_D0); + g2.setColor(TRANSLUCENT_BACKGROUND); g2.fillRect(0, 0, getWidth(), getHeight()); } } diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/Colors.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/Colors.java index bc1b3b83..80663524 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/Colors.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/Colors.java @@ -20,6 +20,18 @@ * Various color related utilities. */ public class Colors { + /** + * Hue + */ + public static final int H = 0; + /** + * Saturation + */ + public static final int S = 1; + /** + * Luminance + */ + public static final int L = 2; private static volatile boolean darkMode = false; /** @@ -35,40 +47,64 @@ public class Colors { */ public static final int DARK_PERCEIVED_BRIGHTNESS_THRESHOLD = gammaFunction(0.45); - /** Color BLACK with alpha 0xD0. */ + /** + * Color BLACK with alpha 0xD0. + */ public static Color translucent_black_D0 = new Color(0xD0000000, true); - /** Color BLACK with alpha 0xB0. */ + /** + * Color BLACK with alpha 0xB0. + */ public static Color translucent_black_B0 = new Color(0xB0000000, true); - /** Color BLACK with alpha 0x80. */ + /** + * Color BLACK with alpha 0x80. + */ public static Color translucent_black_80 = new Color(0x80000000, true); - /** Color BLACK with alpha 0x60. */ + /** + * Color BLACK with alpha 0x60. + */ public static Color translucent_black_60 = new Color(0x60000000, true); - /** Color BLACK with alpha 0x40. */ + /** + * Color BLACK with alpha 0x40. + */ public static Color translucent_black_40 = new Color(0x40000000, true); - /** Color BLACK with alpha 0x20. */ + /** + * Color BLACK with alpha 0x20. + */ public static Color translucent_black_20 = new Color(0x20000000, true); - /** Color WHITE with alpha 0xD0. */ + /** + * Color WHITE with alpha 0xD0. + */ public static Color translucent_white_D0 = new Color(0xD0FFFFFF, true); - /** Color WHITE with alpha 0xB0. */ + /** + * Color WHITE with alpha 0xB0. + */ public static Color translucent_white_B0 = new Color(0xB0FFFFFF, true); - /** Color WHITE with alpha 0x80. */ + /** + * Color WHITE with alpha 0x80. + */ public static Color translucent_white_80 = new Color(0x80FFFFFF, true); - /** Color WHITE with alpha 0x60. */ + /** + * Color WHITE with alpha 0x60. + */ public static Color translucent_white_60 = new Color(0x60FFFFFF, true); - /** Color WHITE with alpha 0x40. */ + /** + * Color WHITE with alpha 0x40. + */ public static Color translucent_white_40 = new Color(0x40FFFFFF, true); - /** Color WHITE with alpha 0x20. */ + /** + * Color WHITE with alpha 0x20. + */ public static Color translucent_white_20 = new Color(0x20FFFFFF, true); /** @@ -175,7 +211,7 @@ public enum Palette { ), /** - * Light rainbow color palette. + * Light rainbow color palette. */ LIGHT_BLUE_GREEN_ORANGE_RED( /*blue*/ "#003565", "#004F99", "#1272CB", "#0084FF", @@ -187,7 +223,7 @@ public enum Palette { ), /** - * Datadog color palette. + * Datadog color palette. */ DATADOG( new Color(0x3399CC), @@ -213,7 +249,7 @@ public enum Palette { ), /** - * Pyroscope color palette. + * Pyroscope color palette. */ PYROSCOPE( new Color(0xDF8B53), @@ -356,6 +392,112 @@ public static Color blend(Color c0, Color c1) { return new Color((int) r, (int) g, (int) b, (int) a); } + /** + * Dim the given color and returns a {@link #darkMode} aware color. + * + * @param color The color to dim + * @return The dimmed color ({@link #darkMode} aware) + * @see DarkLightColor + */ + public static Color dim(Color color) { + var hslLight = hslComponents(color); + var hslDark = Arrays.copyOf(hslLight, hslLight.length); + + // if (darkMode) { + // if color is grayish, keep the saturation, otherwise set it to 0.2 + hslDark[S] = hslDark[S] < 0.1f ? hslDark[S] : 0.2f; + hslDark[L] = 0.2f; + // } else { + // if color is grayish, keep the saturation, otherwise set it to 0.4 + hslLight[S] = hslLight[S] < 0.2 ? hslLight[S] : 0.4f; + hslLight[L] = 0.93f; + // } + + return new DarkLightColor( + hsl(hslLight[0], hslLight[1], hslLight[2], color.getAlpha() / 255.0f), + hsl(hslDark[0], hslDark[1], hslDark[2], color.getAlpha() / 255.0f) + ); + } + + + /** + * Convert an RGB Color to it corresponding HSL components. + *

+ * From d3-colors. + * + * @param color The color to convert + * @return an array containing the 3 HSL values. + */ + public static float[] hslComponents(Color color) { + float r = color.getRed() / 255.0f; + float g = color.getGreen() / 255.0f; + float b = color.getBlue() / 255.0f; + + float min = Math.min(r, Math.min(g, b)), + max = Math.max(r, Math.max(g, b)), + h = 0f, + s = max - min, + l = (max + min) / 2; + if (s >= 0) { + if (r == max) { + h = (g - b) / s + ((g < b) ? 6 : 0); + } else if (g == max) { + h = (b - r) / s + 2; + } else { + h = (r - g) / s + 4; + } + s /= l < 0.5 ? max + min : 2 - max - min; + h *= 60; + } else { + s = ((l > 0) && (l < 1)) ? 0 : h; + } + + return new float[]{h, s, l}; + } + + /** + * Convert HSL values to a RGB Color. + *

+ * From d3-colors. + * + * @param h Hue is specified as degrees in the range 0 - 360. + * @param s Saturation is specified as a percentage in the range 1 - 100. + * @param l Luminance is specified as a percentage in the range 1 - 100. + * @param alpha the alpha value between 0 - 1 + * @return the RGB Color object + */ + public static Color hsl(float h, float s, float l, float alpha) { + h = h % 360 + ((h < 0) ? 360 : 0); + s = Float.isNaN(h) || Float.isNaN(s) ? 0 : s; + + float m2 = l + (l < 0.5 ? l : 1 - l) * s, + m1 = 2 * l - m2; + + return new Color( + ((int) (alpha * 255)) << 24 + | hueToRgb(h >= 240 ? h - 240 : h + 120, m1, m2) << 16 + | hueToRgb(h, m1, m2) << 8 + | hueToRgb(h < 120 ? h + 240 : h - 120, m1, m2) + + ); + } + + /* From FvD 13.37, CSS Color Module Level 3 */ + private static int hueToRgb(float h, float m1, float m2) { + return (int) (((h < 60) ? (m1 + (m2 - m1) * h / 60) + : ((h < 180) ? m2 + : ((h < 240) ? (m1 + (m2 - m1) * (240 - h) / 60) + : m1 + ) + ) + ) * 255); + } + + public static Color rgba(int r, int g, int b, float a) { + return new Color(r, g, b, (int) (a * 255)); + } + + /** * Utility method to print out the colors for the look and feel defaults. */ diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/JScrollPaneWithButton.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/JScrollPaneWithButton.java index d7e01a32..1769fe87 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/JScrollPaneWithButton.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/core/ui/JScrollPaneWithButton.java @@ -42,8 +42,14 @@ private static class ScrollBackToTopLayerUI extends LayerUI { public int yGap; private final JPanel buttonContainer = new JPanel(); private final Point currentMousePoint = new Point(); - private final JButton button = new JButton(new UpArrowIcon(new Color(0xAA_3D_42_44, true), - new Color(0xAA_38_9F_D6, true))) { + private final JButton button = new JButton(new UpArrowIcon(new Color(0xAA3D4244, true), + new Color(0xAA389FD6, true))) { + + private final Color ARMED_BUTTON_COLOR = new DarkLightColor( + Color.darkGray, + Color.lightGray + ); + @Override public void updateUI() { super.updateUI(); @@ -62,7 +68,7 @@ protected void paintComponent(Graphics g) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); if (getModel().isArmed()) { - g2.setColor(Colors.isDarkMode() ? Color.lightGray : Color.darkGray); + g2.setColor(ARMED_BUTTON_COLOR); } else { g2.setColor(UIManager.getColor("Button.background")); } diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameColorProvider.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameColorProvider.java index 2d3c5a1d..ceef36d8 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameColorProvider.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameColorProvider.java @@ -33,6 +33,7 @@ * * @param The type of the frame node (depends on the source of profiling data). */ +@FunctionalInterface public interface FrameColorProvider { class ColorModel { public Color background; @@ -44,12 +45,12 @@ class ColorModel { * @param background The background color of the frame. * @param foreground The foreground color of the frame. */ - ColorModel(Color background, Color foreground) { + public ColorModel(Color background, Color foreground) { this.background = background; this.foreground = foreground; } - ColorModel set(Color background, Color foreground) { + public ColorModel set(Color background, Color foreground) { this.background = background; this.foreground = foreground; return this; diff --git a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameFontProvider.java b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameFontProvider.java index d154bf77..d1fd64ca 100644 --- a/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameFontProvider.java +++ b/fireplace-swing/src/main/java/io/github/bric3/fireplace/flamegraph/FrameFontProvider.java @@ -26,6 +26,7 @@ * * @param The type of the frame node (depends on the source of profiling data). */ +@FunctionalInterface public interface FrameFontProvider { /** @@ -47,35 +48,39 @@ static FrameFontProvider defaultFontProvider() { /** * The font used to display frame labels */ - private final Font frameLabelFont = new Font(Font.SANS_SERIF, Font.PLAIN, 12); + private final Font regular = new Font(Font.SANS_SERIF, Font.PLAIN, 12); /** * 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 final Font partialFrameLabelFont = new Font(Font.SANS_SERIF, Font.ITALIC, 12); + private final Font italic = new Font(Font.SANS_SERIF, Font.ITALIC, 12); /** * The font used to display frame labels */ - private final Font highlightedFrameLabelFont = new Font(Font.SANS_SERIF, Font.PLAIN | Font.BOLD, 12); + private final Font bold = new Font(Font.SANS_SERIF, Font.PLAIN | Font.BOLD, 12); /** * 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 final Font highlightedPartialFrameLabelFont = new Font(Font.SANS_SERIF, Font.ITALIC | Font.BOLD, 12); + private final Font italicBold = new Font(Font.SANS_SERIF, Font.ITALIC | Font.BOLD, 12); @Override public Font getFont(FrameBox frame, int flags) { + if (frame != null && frame.isRoot()) { + return bold; + } + if (isHighlightedFrame(flags)) { // when parent frame are larger than view port - return isPartialFrame(flags) ? highlightedPartialFrameLabelFont : highlightedFrameLabelFont; + return isPartialFrame(flags) ? italicBold : bold; } // when parent frame are larger than view port - return isPartialFrame(flags) ? partialFrameLabelFont : frameLabelFont; + return isPartialFrame(flags) ? italic : regular; } }; } diff --git a/fireplace-swt-experiment/src/main/java/io/github/bric3/fireplace/FirePlaceSwtMain.java b/fireplace-swt-experiment/src/main/java/io/github/bric3/fireplace/FirePlaceSwtMain.java index bc5fb330..66773f72 100644 --- a/fireplace-swt-experiment/src/main/java/io/github/bric3/fireplace/FirePlaceSwtMain.java +++ b/fireplace-swt-experiment/src/main/java/io/github/bric3/fireplace/FirePlaceSwtMain.java @@ -4,6 +4,8 @@ import io.github.bric3.fireplace.flamegraph.ColorMapper; import io.github.bric3.fireplace.flamegraph.FlamegraphView; import io.github.bric3.fireplace.flamegraph.FrameBox; +import io.github.bric3.fireplace.flamegraph.FrameColorProvider; +import io.github.bric3.fireplace.flamegraph.FrameFontProvider; import io.github.bric3.fireplace.flamegraph.FrameTextsProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.awt.SWT_AWT; @@ -186,7 +188,10 @@ private void loadJfr(String[] args, Label text, FlamegraphView fg) { frame -> frame.isRoot() ? "" : FormatToolkit.getHumanReadable(frame.actualNode.getFrame().getMethod(), false, false, false, false, true, false), frame -> frame.isRoot() ? "" : frame.actualNode.getFrame().getMethod().getMethodName() ), - frame -> ColorMapper.ofObjectHashUsing(Palette.DATADOG.colors()).apply(frame.actualNode.getFrame().getMethod().getType().getPackage()), + FrameColorProvider.defaultColorProvider( + frame -> ColorMapper.ofObjectHashUsing(Palette.DATADOG.colors()).apply(frame.actualNode.getFrame().getMethod().getType().getPackage()) + ), + FrameFontProvider.defaultFontProvider(), frame -> "" ); flameGraph.requestRepaint();