Repaint() duplicates a sibling component. Why?

I was experimenting with javax.swing. Here’s a simple task I was going to do: to make a circle change color once a button (a sibling component) is pressed. Here’s my code. I’m enthusiastic about design patterns so you see all these singletons, builders, and whatnot (I hope you don’t mind)

public class App {
    public static void main(String[] args) {
        MyUI ui = new MyUI();
        ui.display();
    }
}
public class MyUI {
    public void display() {
        UIUtil.getUIBuilder()
                .withButton(SOUTH, () -> {
                    JButton button = new JButton("click me!");
                    button.setFont(new Font(SANS_SERIF, BOLD, 28));
                    button.setSize(150, 50);
                    button.addMouseListener(MouseListeningPanel.getInstance());
                    return button;
                })
                .withPanel(CENTER, MouseListeningPanel::getInstance)
                .withFrameSize(300, 300)
                .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
                .visualize();
    }
}
public class MouseListeningPanel extends JPanel implements MouseListener {
    public static final MouseListeningPanel INSTANCE = new MouseListeningPanel();
    private Color startColor = UIUtil.getRandomColor();
    private Color endColor = UIUtil.getRandomColor();
    private MouseListeningPanel() {}
    public static MouseListeningPanel getInstance() {
        return INSTANCE;
    }

    @Override
    protected void paintComponent(Graphics g) {
        Graphics2D twoDGraphics = (Graphics2D) g;
        GradientPaint gradient = new GradientPaint(0, 0, startColor, getMiddle(), getMiddle(), endColor);
        twoDGraphics.setPaint(gradient);
        twoDGraphics.fillOval(calculateX(), calculateY(), calculateWidth(), calculateWidth());
    }

    private float getMiddle() {
        return getWidth() / 2f;
    }

    private int calculateX() {
        return getWidth() / 2 - calculateWidth() / 2;
    }

    private int calculateY() {
        return getHeight() / 2 - calculateWidth() / 2;
    }

    private int calculateWidth() {
        return Math.min(getWidth(), getHeight()) / 2;
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        startColor = UIUtil.getRandomColor();
        endColor = UIUtil.getRandomColor();
        repaint();
    }

    // dummy implementations for other MouseListener methods
    // I can't extend JPanel and MouseAdapter at the same time so I have to provide them
}
public class UIUtil {

    public static UIBuilder getUIBuilder() {
        return new UIBuilder();
    }
    public static UIBuilder getUIBuilder(Supplier<Container> contentPaneSupplier) {
        return new UIBuilder(contentPaneSupplier);
    }
    public static Color getRandomColor() {
        int red = Util.randomInt(255);
        int green = Util.randomInt(255);
        int blue = Util.randomInt(255);
        return new Color(red, green, blue);
    }

    public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight) {
        return getRandomRectangle(parentWidth, parentHeight, 0.5F);
    }

    public static SimpleRectangle getRandomRectangle(int parentWidth, int parentHeight, float recDimensionToParentDimensionRatio) {
        int x = Util.randomInt(parentWidth);
        int y = Util.randomInt(parentHeight);
        int width = Util.randomInt((int) (parentWidth * recDimensionToParentDimensionRatio));
        int height = Util.randomInt((int) (parentHeight * recDimensionToParentDimensionRatio));
        return new SimpleRectangle(x, y, width, height);
    }

    public record SimpleRectangle(int x, int y, int width, int height) {}

    @NoArgsConstructor
    public static class UIBuilder {
        private final JFrame frame;
        private final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

        {
            frame = new JFrame();
            this.withFrameSize(300, 300)
            .withDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }

        public UIBuilder(@NotNull Supplier<Container> contentPaneSupplier) {
            Objects.requireNonNull(contentPaneSupplier);
            frame.setContentPane(contentPaneSupplier.get());
        }

        public UIBuilder withButton(@NotNull String position, @NotNull Supplier<JButton> buttonSupplier) {
            Stream.of(position, buttonSupplier).forEach(Objects::requireNonNull);
            JButton button = buttonSupplier.get();
            frame.getContentPane().add(position, button);
            return this;
        }

        public UIBuilder withPanel(@NotNull String position, @NotNull Supplier<JPanel> panelSupplier) {
            Stream.of(position, panelSupplier).forEach(Objects::requireNonNull);
            JPanel panel = panelSupplier.get();
            JScrollPane scrollPane = new JScrollPane(panel);
            frame.getContentPane().add(position, scrollPane);
            return this;
        }

        public UIBuilder withFrameSize(int width, int height) {
            checkAgainstScreenSize(width, height);
            int x = (screenSize.width - width) / 2;
            int y = (screenSize.height - height) / 2;
            frame.setBounds(x, y, width, height);
            return this;
        }

        private void checkAgainstScreenSize(int width, int height) {
            if (screenSize.width < width) {
                throw new IllegalArgumentException("Frame width cannot be greater than screen width");
            } else if (screenSize.height < height) {
                throw new IllegalArgumentException("Frame height cannot be greater than screen height");
            }
        }

        public UIBuilder withDefaultCloseOperation(int windowConstant) {
            frame.setDefaultCloseOperation(windowConstant);
            return this;
        }

        public void visualize() {
            frame.setVisible(true);
        }
    }
}

It kind of worked, but there was a little snag: once I pressed the button, it got cloned, and the copy was put in the NORTH section of the pane. I can’t imagine why Java did that

broken UI

Since regular resizing didn’t produce that result and mouseClicked() only sets colors and calls repaint(), repaint() is the only suspect. I checked the doc for the method

Repaints this component

This! The button is not “this”, is it? The button is a sibling, not a component of the panel. “This” must refer to the panel itself, and its paintComponent() doesn’t add any buttons

Thanks to StackOverflow, I fixed the issue by adding super.paintComponent(g) first line

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
    // ...
    }

But I’m still puzzled as to how the lack of that line was connected to the button issue. They said on the StackOverflow that it clears the canvas somehow, but I don’t see what the cloned button has to do with the canvas not being cleared

Why did the problem occur, and how did super.paintComponent(g) fix it exactly?