···11+package util.ui;
22+33+import java.awt.Color;
44+import java.awt.Component;
55+import java.awt.Graphics;
66+import java.awt.MouseInfo;
77+import java.awt.Point;
88+import java.awt.Rectangle;
99+1010+import javax.swing.JScrollPane;
1111+import javax.swing.Timer;
1212+import javax.swing.UIManager;
1313+1414+/**
1515+ * A JScrollPane with macOS-style scrollbars that fade in on hover and fade out when not in use.
1616+ */
1717+public class FadingScrollPane extends JScrollPane
1818+{
1919+ private static final float FADE_SPEED = 0.1f; // alpha change per repaint
2020+ private static final int CHECK_INTERVAL = 50; // milliseconds
2121+2222+ private float currentAlpha = 0.0f;
2323+ private boolean lastMouseInside = false;
2424+2525+ public FadingScrollPane(Component view)
2626+ {
2727+ super(view);
2828+2929+ setBorder(null);
3030+ setOpaque(false);
3131+ getViewport().setOpaque(false);
3232+ getViewport().setBackground(new Color(0, 0, 0, 0));
3333+3434+ // Configure scrollbars - make everything transparent
3535+ var vScrollBar = getVerticalScrollBar();
3636+ var hScrollBar = getHorizontalScrollBar();
3737+3838+ vScrollBar.setOpaque(false);
3939+ hScrollBar.setOpaque(false);
4040+ vScrollBar.setBackground(new Color(0, 0, 0, 0));
4141+ hScrollBar.setBackground(new Color(0, 0, 0, 0));
4242+ vScrollBar.setBorder(null);
4343+ hScrollBar.setBorder(null);
4444+ vScrollBar.putClientProperty("JScrollBar.showButtons", false);
4545+ hScrollBar.putClientProperty("JScrollBar.showButtons", false);
4646+4747+ // Set initial style with fully transparent scrollbar
4848+ String initialStyle = "track: #00000000; thumb: #00000000";
4949+ vScrollBar.putClientProperty("FlatLaf.style", initialStyle);
5050+ hScrollBar.putClientProperty("FlatLaf.style", initialStyle);
5151+5252+ putClientProperty("JScrollPane.smoothScrolling", true);
5353+5454+ // Periodically check mouse position and trigger repaints as needed
5555+ new Timer(CHECK_INTERVAL, e -> {
5656+ boolean mouseInside = isMouseInside();
5757+ if (mouseInside != lastMouseInside) {
5858+ lastMouseInside = mouseInside;
5959+ repaint();
6060+ }
6161+ }).start();
6262+ }
6363+6464+ private boolean isMouseInside()
6565+ {
6666+ try {
6767+ Point mousePos = MouseInfo.getPointerInfo().getLocation();
6868+ Point componentPos = getLocationOnScreen();
6969+ Rectangle bounds = new Rectangle(componentPos.x, componentPos.y, getWidth(), getHeight());
7070+ return bounds.contains(mousePos);
7171+ }
7272+ catch (Exception e) {
7373+ return false;
7474+ }
7575+ }
7676+7777+ @Override
7878+ public void paint(Graphics g)
7979+ {
8080+ updateScrollbarColors();
8181+ super.paint(g);
8282+ }
8383+8484+ private void updateScrollbarColors()
8585+ {
8686+ float targetAlpha = isMouseInside() ? 1.0f : 0.0f;
8787+8888+ // Interpolate current alpha toward target
8989+ if (currentAlpha < targetAlpha) {
9090+ currentAlpha = Math.min(targetAlpha, currentAlpha + FADE_SPEED);
9191+ }
9292+ else if (currentAlpha > targetAlpha) {
9393+ currentAlpha = Math.max(targetAlpha, currentAlpha - FADE_SPEED);
9494+ }
9595+9696+ Color baseThumb = UIManager.getColor("ScrollBar.thumb");
9797+ if (baseThumb == null) {
9898+ baseThumb = new Color(128, 128, 128);
9999+ }
100100+101101+ // Apply alpha to thumb
102102+ Color thumbColor = new Color(
103103+ baseThumb.getRed(),
104104+ baseThumb.getGreen(),
105105+ baseThumb.getBlue(),
106106+ (int) (currentAlpha * 255)
107107+ );
108108+109109+ String style = String.format(
110110+ "track: #00000000; thumb: #%02x%02x%02x%02x",
111111+ thumbColor.getRed(), thumbColor.getGreen(), thumbColor.getBlue(), thumbColor.getAlpha()
112112+ );
113113+114114+ getVerticalScrollBar().putClientProperty("FlatLaf.style", style);
115115+ getHorizontalScrollBar().putClientProperty("FlatLaf.style", style);
116116+117117+ // Continue animation if not at target
118118+ if (currentAlpha != targetAlpha) {
119119+ repaint();
120120+ }
121121+ }
122122+}
+67
src/main/java/util/ui/Squircle.java
···11+package util.ui;
22+33+import java.awt.geom.Path2D;
44+55+public abstract class Squircle
66+{
77+ public static Path2D.Double path(double x, double y, double w, double h, double arc)
88+ {
99+ var path = new Path2D.Double();
1010+1111+ path.moveTo(x + w - arc, y);
1212+ path.lineTo(x + arc, y);
1313+ path.quadTo(x, y, x, y + arc);
1414+ path.lineTo(x, y + h - arc);
1515+ path.quadTo(x, y + h, x + arc, y + h);
1616+ path.lineTo(x + w - arc, y + h);
1717+ path.quadTo(x + w, y + h, x + w, y + h - arc);
1818+ path.lineTo(x + w, y + arc);
1919+ path.quadTo(x + w, y, x + w - arc, y);
2020+ path.closePath();
2121+2222+ return path;
2323+ }
2424+2525+ public static Path2D.Double path(double x, double y, double w, double h, double topLeft, double topRight, double bottomRight, double bottomLeft)
2626+ {
2727+ var path = new Path2D.Double();
2828+2929+ // Start at top edge after top-right corner
3030+ path.moveTo(x + w - topRight, y);
3131+ // Top edge to top-left corner
3232+ if (topLeft > 0) {
3333+ path.lineTo(x + topLeft, y);
3434+ path.quadTo(x, y, x, y + topLeft);
3535+ }
3636+ else {
3737+ path.lineTo(x, y);
3838+ }
3939+ // Left edge
4040+ if (bottomLeft > 0) {
4141+ path.lineTo(x, y + h - bottomLeft);
4242+ path.quadTo(x, y + h, x + bottomLeft, y + h);
4343+ }
4444+ else {
4545+ path.lineTo(x, y + h);
4646+ }
4747+ // Bottom edge
4848+ if (bottomRight > 0) {
4949+ path.lineTo(x + w - bottomRight, y + h);
5050+ path.quadTo(x + w, y + h, x + w, y + h - bottomRight);
5151+ }
5252+ else {
5353+ path.lineTo(x + w, y + h);
5454+ }
5555+ // Right edge
5656+ if (topRight > 0) {
5757+ path.lineTo(x + w, y + topRight);
5858+ path.quadTo(x + w, y, x + w - topRight, y);
5959+ }
6060+ else {
6161+ path.lineTo(x + w, y);
6262+ }
6363+ path.closePath();
6464+6565+ return path;
6666+ }
6767+}
+4
src/main/java/util/ui/ThemedIcon.java
···83838484 public static final FlatSVGIcon PACKAGE_24 = getIcon("package");
8585 public static final FlatSVGIcon PACKAGE_16 = PACKAGE_24.derive(16, 16);
8686+8787+ public static final FlatSVGIcon GIT_BRANCH = getIcon("git_branch");
8888+8989+ public static final FlatSVGIcon SEARCH = getIcon("search");
8690}