diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 63bb74512..c7a99881d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -18,8 +18,6 @@ package org.jackhuang.hmcl.ui; import javafx.beans.value.ObservableValue; -import javafx.scene.Group; -import javafx.scene.Node; import javafx.scene.paint.Paint; import javafx.scene.shape.SVGPath; @@ -30,6 +28,7 @@ import javafx.scene.shape.SVGPath; /// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px. /// The view boxes of all icons are normalized to `0 0 24 24`. public enum SVG { + NONE(""), // Empty Icon ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"), ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"), ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material @@ -130,6 +129,12 @@ public enum SVG { public static final double DEFAULT_SIZE = 24; + static void setSize(SVGPath path, double size) { + double scale = size / DEFAULT_SIZE; + path.setScaleX(scale); + path.setScaleY(scale); + } + private final String rawPath; private String path; @@ -144,35 +149,20 @@ public enum SVG { return path; } - public SVGPath createSVGPath() { - var p = new SVGPath(); - p.setContent(getPath()); - p.getStyleClass().add("svg"); - return p; + public SVGPath createIcon() { + var path = new SVGPath(); + path.getStyleClass().add("svg"); + path.setContent(getPath()); + return path; } - private static Node createIcon(SVGPath path, double size) { - if (size == DEFAULT_SIZE) - return path; - else { - double scale = size / DEFAULT_SIZE; - path.setScaleX(scale); - path.setScaleY(scale); - return new Group(path); - } + public SVGContainer createIcon(double size) { + return new SVGContainer(this, size); } - public Node createIcon() { - return createIcon(DEFAULT_SIZE); - } - - public Node createIcon(double size) { - return createIcon(createSVGPath(), size); - } - - public Node createIcon(ObservableValue color) { - SVGPath p = createSVGPath(); - p.fillProperty().bind(color); - return createIcon(p, DEFAULT_SIZE); + public SVGPath createIcon(ObservableValue color) { + SVGPath path = createIcon(); + path.fillProperty().bind(color); + return path; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVGContainer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVGContainer.java new file mode 100644 index 000000000..ea7ffd4a1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVGContainer.java @@ -0,0 +1,175 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * 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 org.jackhuang.hmcl.ui; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Parent; +import javafx.scene.shape.SVGPath; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.animation.Motion; + +/// A lightweight wrapper for displaying [SVG] icons. +/// +/// @author Glavo +public final class SVGContainer extends Parent { + + private static final String DEFAULT_STYLE_CLASS = "svg-container"; + + private final SVGPath path = new SVGPath(); + private SVG icon = SVG.NONE; + private double iconSize = SVG.DEFAULT_SIZE; + private SVGPath tempPath; + private Timeline timeline; + + { + this.getStyleClass().add(DEFAULT_STYLE_CLASS); + this.path.getStyleClass().add("svg"); + } + + /// Creates an SVGContainer with the default icon and the default icon size. + public SVGContainer() { + this(SVG.NONE, SVG.DEFAULT_SIZE); + } + + /// Creates an SVGContainer showing the given icon using the default icon size. + /// + /// @param icon the [SVG] icon to display + public SVGContainer(SVG icon) { + this(icon, SVG.DEFAULT_SIZE); + } + + /// Creates an SVGContainer with a custom icon size. The initial icon is + /// [SVG#NONE]. + /// + /// @param iconSize the icon size + public SVGContainer(double iconSize) { + this(SVG.NONE, iconSize); + } + + /// Creates an SVGContainer with the specified icon and size. + /// + /// @param icon the [SVG] icon to display + /// @param iconSize the icon size + public SVGContainer(SVG icon, double iconSize) { + setIconSizeImpl(iconSize); + setIcon(icon); + } + + public double getIconSize() { + return iconSize; + } + + private void setIconSizeImpl(double newSize) { + this.iconSize = newSize; + SVG.setSize(path, newSize); + if (tempPath != null) + SVG.setSize(tempPath, newSize); + } + + public void setIconSize(double newSize) { + setIconSizeImpl(newSize); + requestLayout(); + } + + /// Gets the currently displayed icon. + public SVG getIcon() { + return icon; + } + + /// Sets the icon to display without animation. + public void setIcon(SVG newIcon) { + setIcon(newIcon, Duration.ZERO); + } + + /// Sets the icon to display with a cross-fade animation. + public void setIcon(SVG newIcon, Duration animationDuration) { + if (timeline != null) { + timeline.stop(); + timeline = null; + } + + SVG oldIcon = this.icon; + this.icon = newIcon; + + if (animationDuration.equals(Duration.ZERO)) { + path.setContent(newIcon.getPath()); + path.setOpacity(1); + if (getChildren().size() != 1) + getChildren().setAll(path); + } else { + if (tempPath == null) { + tempPath = new SVGPath(); + tempPath.getStyleClass().add("svg"); + SVG.setSize(tempPath, iconSize); + } else + tempPath.setOpacity(1); + + tempPath.setContent(oldIcon.getPath()); + getChildren().setAll(path, tempPath); + + path.setOpacity(0); + path.setContent(newIcon.getPath()); + + timeline = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(path.opacityProperty(), 0, Motion.LINEAR), + new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR) + ), + new KeyFrame(animationDuration, + new KeyValue(path.opacityProperty(), 1, Motion.LINEAR), + new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR) + ) + ); + timeline.setOnFinished(e -> { + getChildren().setAll(path); + timeline = null; + }); + timeline.play(); + } + } + + // Parent + + @Override + public double prefWidth(double height) { + return iconSize; + } + + @Override + public double prefHeight(double width) { + return iconSize; + } + + @Override + public double minHeight(double width) { + return iconSize; + } + + @Override + public double minWidth(double height) { + return iconSize; + } + + @Override + protected void layoutChildren() { + // Do nothing + } + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java index 3a021fdb6..b0eb8fc57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/AdvancedListBox.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.construct; import javafx.collections.ObservableList; -import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.ScrollPane; @@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.SVGContainer; +import org.jackhuang.hmcl.ui.animation.Motion; import java.util.function.Consumer; @@ -100,27 +99,17 @@ public class AdvancedListBox extends ScrollPane { return add(item); } - @SuppressWarnings("SuspiciousNameCombination") public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab tab, String title, SVG unselectedGraphic, SVG selectedGraphic) { AdvancedListItem item = createNavigationDrawerItem(title, null); item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab)); item.setOnAction(e -> tabHeader.select(tab)); - Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE); - Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE); - - TransitionPane leftGraphic = new TransitionPane(); - AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER); + var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE); leftGraphic.setMouseTransparent(true); - leftGraphic.setAlignment(Pos.CENTER); - FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE); - FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE); - leftGraphic.setPadding(Insets.EMPTY); - leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE); - FXUtils.onChange(item.activeProperty(), active -> - leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE)); - + AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER); + AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN); + FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4)); item.setLeftGraphic(leftGraphic); return add(item); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index bce949e5f..60852167a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXListView; import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; @@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; import javafx.stage.FileChooser; import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.setting.Profile; @@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase impl private final HBox right; private final ImageView iconImageView; - private final SVGPath iconSVG; - private final StackPane iconSVGWrapper; + private final SVGContainer iconSVGView; private final Tooltip tooltip = new Tooltip(); @@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase impl this.iconImageView = new ImageView(); FXUtils.limitSize(iconImageView, 32, 32); - this.iconSVG = new SVGPath(); - iconSVG.getStyleClass().add("svg"); - iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE); - iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE); - - this.iconSVGWrapper = new StackPane(new Group(iconSVG)); - iconSVGWrapper.setAlignment(Pos.CENTER); - FXUtils.setLimitWidth(iconSVGWrapper, 32); - FXUtils.setLimitHeight(iconSVGWrapper, 32); + this.iconSVGView = new SVGContainer(32); BorderPane.setAlignment(left, Pos.CENTER); root.setLeft(left); @@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase impl iconImageView.setImage(fileItem.getImage()); left.getChildren().setAll(iconImageView); } else { - iconSVG.setContent(item.getIcon().getPath()); - left.getChildren().setAll(iconSVGWrapper); + iconSVGView.setIcon(item.getIcon()); + left.getChildren().setAll(iconSVGView); } center.setTitle(item.getName());