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" }