diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 19c09d6e9..4f47dfeab 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -98,6 +98,7 @@ dependencies {
slim(libs.byteBuddy.agent)
slim(libs.dom4j)
slim(libs.jaxen)
+ slim(libs.flatlaf)
// Script Engine
slim(libs.kotlin.stdlib)
diff --git a/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java b/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java
index aac0f7df2..2abd9b6bc 100644
--- a/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java
+++ b/core/src/main/java/com/volmit/iris/core/gui/GuiHost.java
@@ -59,6 +59,7 @@ public final class GuiHost {
public static JFrame createFrame(String title) {
XwaylandWindowManagerHint.apply();
+ StudioTheme.install();
JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
MANAGED_FRAMES.add(frame);
diff --git a/core/src/main/java/com/volmit/iris/core/gui/StudioOverlay.java b/core/src/main/java/com/volmit/iris/core/gui/StudioOverlay.java
new file mode 100644
index 000000000..3f005dd76
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/StudioOverlay.java
@@ -0,0 +1,43 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2022 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.volmit.iris.core.gui;
+
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.RoundRectangle2D;
+
+/**
+ * Draws the floating cards (legends, hover details, hints) on top of a map, styled like IntelliJ's popups.
+ */
+final class StudioOverlay {
+ private StudioOverlay() {
+ }
+
+ static void drawCard(Graphics2D canvas, int x, int y, int width, int height) {
+ Object antialiasing = canvas.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ RoundRectangle2D card = new RoundRectangle2D.Double(x + 0.5D, y + 0.5D, width - 1D, height - 1D,
+ StudioTheme.OVERLAY_ARC, StudioTheme.OVERLAY_ARC);
+ canvas.setColor(StudioTheme.OVERLAY_BACKGROUND);
+ canvas.fill(card);
+ canvas.setColor(StudioTheme.OVERLAY_BORDER);
+ canvas.draw(card);
+ canvas.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing);
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/StudioStatusBar.java b/core/src/main/java/com/volmit/iris/core/gui/StudioStatusBar.java
new file mode 100644
index 000000000..535aa0655
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/StudioStatusBar.java
@@ -0,0 +1,103 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2022 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.volmit.iris.core.gui;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import java.awt.Dimension;
+import java.util.Objects;
+
+/**
+ * An IntelliJ style status bar: what is shown on the left, work in progress in the middle and where the pointer is on the right.
+ */
+final class StudioStatusBar extends JPanel {
+ private static final long serialVersionUID = 6302468216735125611L;
+ private static final int HEIGHT = 26;
+ private static final int PROGRESS_STEPS = 1_000;
+
+ private final JLabel summary = statusLabel();
+ private final JLabel activity = statusLabel();
+ private final JProgressBar progress = new JProgressBar(0, PROGRESS_STEPS);
+ private final JLabel position = statusLabel();
+
+ StudioStatusBar() {
+ setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
+ setBackground(StudioTheme.PANEL_BACKGROUND);
+ setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createMatteBorder(1, 0, 0, 0, StudioTheme.SEPARATOR),
+ BorderFactory.createEmptyBorder(0, 10, 0, 10)
+ ));
+ setPreferredSize(new Dimension(0, HEIGHT));
+ progress.setMaximumSize(new Dimension(120, 4));
+ progress.setPreferredSize(new Dimension(120, 4));
+ progress.setVisible(false);
+ add(summary);
+ add(Box.createHorizontalGlue());
+ add(activity);
+ add(Box.createHorizontalStrut(8));
+ add(progress);
+ add(Box.createHorizontalStrut(18));
+ add(position);
+ }
+
+ void showSummary(String text) {
+ setTextIfChanged(summary, text);
+ summary.setForeground(StudioTheme.SECONDARY_TEXT);
+ }
+
+ void showError(String text) {
+ setTextIfChanged(summary, text);
+ summary.setForeground(StudioTheme.ERROR);
+ }
+
+ void showActivity(String text) {
+ setTextIfChanged(activity, text);
+ }
+
+ void showProgress(double completion) {
+ boolean working = completion < 1D;
+ progress.setVisible(working);
+ if (working) {
+ progress.setValue((int) Math.round(Math.max(0D, completion) * PROGRESS_STEPS));
+ }
+ }
+
+ void showPosition(String text) {
+ setTextIfChanged(position, text);
+ }
+
+ private static JLabel statusLabel() {
+ JLabel label = new JLabel();
+ label.setFont(StudioTheme.smallFont());
+ label.setForeground(StudioTheme.SECONDARY_TEXT);
+ return label;
+ }
+
+ // Status text changes on every mouse move, and an unchanged label shouldn't trigger a relayout.
+ private static void setTextIfChanged(JLabel label, String text) {
+ String next = text == null ? "" : text;
+ if (!Objects.equals(label.getText(), next)) {
+ label.setText(next);
+ }
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java b/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java
new file mode 100644
index 000000000..8d2458043
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/StudioTheme.java
@@ -0,0 +1,137 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2022 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.volmit.iris.core.gui;
+
+import com.formdev.flatlaf.FlatDarkLaf;
+import com.formdev.flatlaf.FlatLaf;
+import com.volmit.iris.Iris;
+
+import javax.swing.UIManager;
+import java.awt.Color;
+import java.awt.Font;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * The colours of IntelliJ's dark "New UI", shared by FlatLaf's controls and everything the studio windows draw themselves.
+ */
+final class StudioTheme {
+ static final Color EDITOR_BACKGROUND = new Color(0x1E1F22);
+ static final Color PANEL_BACKGROUND = new Color(0x2B2D30);
+ static final Color HOVER_BACKGROUND = new Color(0x393B40);
+ static final Color INACTIVE_SELECTION = new Color(0x43454A);
+ static final Color SELECTION = new Color(0x2E436E);
+ static final Color CONTROL_BORDER = new Color(0x4E5157);
+ static final Color SEPARATOR = new Color(0x1E1F22);
+ static final Color TEXT = new Color(0xDFE1E5);
+ static final Color SECONDARY_TEXT = new Color(0x9DA0A8);
+ static final Color MUTED_TEXT = new Color(0x6F737A);
+ static final Color ACCENT = new Color(0x3574F0);
+ static final Color LINK = new Color(0x548AF7);
+ static final Color ERROR = new Color(0xF75464);
+ static final Color WARNING = new Color(0xF2C55C);
+ static final Color SUCCESS = new Color(0x5FB865);
+ static final Color OVERLAY_BACKGROUND = new Color(0x2B, 0x2D, 0x30, 236);
+ static final Color OVERLAY_BORDER = new Color(0x43454A);
+ static final String SECONDARY_TEXT_HTML = "#9DA0A8";
+ static final int OVERLAY_ARC = 10;
+
+ private static final AtomicBoolean INSTALLED = new AtomicBoolean();
+ private static final Map INTELLIJ_NEW_UI_DEFAULTS = Map.ofEntries(
+ Map.entry("@background", "#2B2D30"),
+ Map.entry("@foreground", "#DFE1E5"),
+ Map.entry("@accentColor", "#3574F0"),
+ Map.entry("@selectionBackground", "#2E436E"),
+ Map.entry("@selectionInactiveBackground", "#43454A"),
+ Map.entry("@disabledForeground", "#6F737A"),
+ Map.entry("Component.borderColor", "#4E5157"),
+ Map.entry("Component.focusedBorderColor", "#3574F0"),
+ Map.entry("Component.focusWidth", "1"),
+ Map.entry("Component.innerFocusWidth", "0"),
+ Map.entry("Component.arc", "8"),
+ Map.entry("Button.arc", "8"),
+ Map.entry("TextComponent.arc", "8"),
+ Map.entry("Button.toolbar.hoverBackground", "#393B40"),
+ Map.entry("Button.toolbar.pressedBackground", "#43454A"),
+ Map.entry("Button.toolbar.selectedBackground", "#43454A"),
+ Map.entry("ToggleButton.toolbar.selectedBackground", "#43454A"),
+ Map.entry("List.selectionArc", "6"),
+ Map.entry("List.selectionInsets", "0,6,0,6"),
+ Map.entry("Tree.selectionArc", "6"),
+ Map.entry("Tree.selectionInsets", "0,6,0,6"),
+ Map.entry("ScrollBar.width", "10"),
+ Map.entry("ScrollBar.thumbArc", "999"),
+ Map.entry("ScrollBar.thumbInsets", "2,2,2,2"),
+ Map.entry("ScrollBar.showButtons", "false"),
+ Map.entry("ScrollBar.track", "#2B2D30"),
+ Map.entry("ScrollPane.border", "0,0,0,0"),
+ Map.entry("SplitPane.dividerSize", "1"),
+ Map.entry("SplitPaneDivider.style", "plain"),
+ Map.entry("SplitPane.background", "#1E1F22"),
+ Map.entry("Separator.foreground", "#1E1F22"),
+ Map.entry("ToolTip.background", "#393B40"),
+ Map.entry("ProgressBar.arc", "999"),
+ Map.entry("ProgressBar.horizontalSize", "120,4")
+ );
+
+ private StudioTheme() {
+ }
+
+ static void install() {
+ if (!INSTALLED.compareAndSet(false, true)) {
+ return;
+ }
+ try {
+ FlatLaf.setGlobalExtraDefaults(INTELLIJ_NEW_UI_DEFAULTS);
+ FlatDarkLaf.setup();
+ } catch (Throwable error) {
+ Iris.reportError(error);
+ Iris.warn("Could not apply the Iris studio theme, the studio windows will use Java's default look.");
+ }
+ }
+
+ static Font font() {
+ return themeFont("defaultFont", Font.PLAIN, 13);
+ }
+
+ static Font smallFont() {
+ return themeFont("small.font", Font.PLAIN, 11);
+ }
+
+ static Font boldFont() {
+ return font().deriveFont(Font.BOLD);
+ }
+
+ static Font titleFont() {
+ return font().deriveFont(Font.BOLD, font().getSize2D() + 1F);
+ }
+
+ static Font sectionFont() {
+ return smallFont().deriveFont(Font.BOLD);
+ }
+
+ static Font monospacedFont() {
+ return themeFont("monospaced.font", Font.PLAIN, 12);
+ }
+
+ private static Font themeFont(String key, int fallbackStyle, int fallbackSize) {
+ Font font = UIManager.getFont(key);
+ return font != null ? font : new Font(Font.SANS_SERIF, fallbackStyle, fallbackSize);
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/core/gui/StudioToolbar.java b/core/src/main/java/com/volmit/iris/core/gui/StudioToolbar.java
new file mode 100644
index 000000000..7383024da
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/core/gui/StudioToolbar.java
@@ -0,0 +1,114 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2022 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.volmit.iris.core.gui;
+
+import com.formdev.flatlaf.FlatClientProperties;
+import com.formdev.flatlaf.icons.FlatSearchIcon;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.JToggleButton;
+import javax.swing.JToolBar;
+import javax.swing.ScrollPaneConstants;
+import java.awt.BorderLayout;
+import java.awt.Insets;
+
+/**
+ * Builds the IntelliJ style controls the studio windows share: a flat main toolbar, borderless toolbar buttons and search boxes.
+ */
+final class StudioToolbar {
+ private static final int TOOLBAR_HEIGHT_PADDING = 5;
+
+ private StudioToolbar() {
+ }
+
+ static JToolBar create() {
+ JToolBar toolbar = new JToolBar();
+ toolbar.setFloatable(false);
+ toolbar.setRollover(true);
+ toolbar.setBackground(StudioTheme.PANEL_BACKGROUND);
+ toolbar.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createMatteBorder(0, 0, 1, 0, StudioTheme.SEPARATOR),
+ BorderFactory.createEmptyBorder(TOOLBAR_HEIGHT_PADDING, 8, TOOLBAR_HEIGHT_PADDING, 8)
+ ));
+ return toolbar;
+ }
+
+ static JLabel label(String text) {
+ JLabel label = new JLabel(text);
+ label.setForeground(StudioTheme.SECONDARY_TEXT);
+ label.setBorder(BorderFactory.createEmptyBorder(0, 6, 0, 6));
+ return label;
+ }
+
+ static JButton button(String text, String tooltip, Runnable action) {
+ JButton button = new JButton(text);
+ button.setToolTipText(tooltip);
+ button.setFocusable(false);
+ button.setMargin(new Insets(4, 8, 4, 8));
+ button.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
+ button.addActionListener(event -> action.run());
+ return button;
+ }
+
+ static JToggleButton toggle(String text, String tooltip) {
+ JToggleButton toggle = new JToggleButton(text);
+ toggle.setToolTipText(tooltip);
+ toggle.setFocusable(false);
+ toggle.setMargin(new Insets(4, 8, 4, 8));
+ toggle.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TOOLBAR_BUTTON);
+ return toggle;
+ }
+
+ // An underlined tab like IntelliJ's tool window tabs.
+ static JToggleButton tab(String text) {
+ JToggleButton tab = new JToggleButton(text);
+ tab.setFocusable(false);
+ tab.putClientProperty(FlatClientProperties.BUTTON_TYPE, FlatClientProperties.BUTTON_TYPE_TAB);
+ return tab;
+ }
+
+ static void makeSearchField(JTextField field, String placeholder) {
+ field.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, placeholder);
+ field.putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_ICON, new FlatSearchIcon());
+ field.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
+ field.setToolTipText(placeholder);
+ }
+
+ static JPanel padded(JComponent content, int top, int left, int bottom, int right) {
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.setOpaque(false);
+ panel.setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
+ panel.add(content, BorderLayout.CENTER);
+ return panel;
+ }
+
+ static JScrollPane scroll(JComponent content) {
+ JScrollPane scrollPane = new JScrollPane(content);
+ scrollPane.setBorder(BorderFactory.createEmptyBorder());
+ scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ scrollPane.getVerticalScrollBar().setUnitIncrement(18);
+ return scrollPane;
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5c1c416f5..b6687df11 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -36,6 +36,7 @@ caffeine = "3.2.1" # https://central.sonatype.com/artifact/com.github.ben-manes.
byte-buddy = "1.17.6" # https://central.sonatype.com/artifact/net.bytebuddy/byte-buddy
dom4j = "2.2.0" # https://central.sonatype.com/artifact/org.dom4j/dom4j
jaxen = "2.0.0" # https://central.sonatype.com/artifact/jaxen/jaxen
+flatlaf = "3.7.2" # https://central.sonatype.com/artifact/com.formdev/flatlaf
# Script Engine
kotlin = "2.2.0"
@@ -84,6 +85,7 @@ byteBuddy-core = { module = "net.bytebuddy:byte-buddy", version.ref = "byte-budd
byteBuddy-agent = { module = "net.bytebuddy:byte-buddy-agent", version.ref = "byte-buddy" }
dom4j = { module = "org.dom4j:dom4j", version.ref = "dom4j" }
jaxen = { module = "jaxen:jaxen", version.ref = "jaxen" }
+flatlaf = { module = "com.formdev:flatlaf", version.ref = "flatlaf" }
# Script Engine
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }