diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXToggleButton.java b/HMCL/src/main/java/com/jfoenix/controls/JFXToggleButton.java
index 60187da4e94..7edfd0c2727 100644
--- a/HMCL/src/main/java/com/jfoenix/controls/JFXToggleButton.java
+++ b/HMCL/src/main/java/com/jfoenix/controls/JFXToggleButton.java
@@ -20,354 +20,373 @@
package com.jfoenix.controls;
import com.jfoenix.skins.JFXToggleButtonSkin;
-import javafx.css.*;
+import javafx.css.CssMetaData;
+import javafx.css.SimpleStyleableBooleanProperty;
+import javafx.css.SimpleStyleableDoubleProperty;
+import javafx.css.Styleable;
+import javafx.css.StyleableBooleanProperty;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
-import javafx.css.converter.PaintConverter;
+import javafx.css.converter.SizeConverter;
+import javafx.geometry.Pos;
+import javafx.scene.AccessibleAttribute;
+import javafx.scene.AccessibleRole;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleButton;
-import javafx.scene.paint.Color;
-import javafx.scene.paint.Paint;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
+import org.jetbrains.annotations.NotNullByDefault;
+import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
-/**
- * JFXToggleButton is the material design implementation of a toggle button.
- * important CSS Selectors:
- *
- * .jfx-toggle-button{
- * -fx-toggle-color: color-value;
- * -fx-untoggle-color: color-value;
- * -fx-toggle-line-color: color-value;
- * -fx-untoggle-line-color: color-value;
- * }
- *
- * To change the rippler color when toggled:
- *
- * .jfx-toggle-button .jfx-rippler{
- * -fx-rippler-fill: color-value;
- * }
- *
- * .jfx-toggle-button:selected .jfx-rippler{
- * -fx-rippler-fill: color-value;
- * }
- *
- * @author Shadi Shaheen
- * @version 1.0
- * @since 2016-03-09
- */
+/// A Material Design 3 switch exposed through the historical JFoenix toggle button type.
+///
+/// The class keeps the `JFXToggleButton` name and selected-state contract used by HMCL, while its default skin
+/// renders an M3 switch track, thumb, touch target, state layer, and selection animation.
+@NotNullByDefault
public class JFXToggleButton extends ToggleButton {
+ /// The legacy style class retained for existing HMCL stylesheets.
+ public static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
+
+ /// The Material switch style class used by the replacement skin.
+ public static final String M3_STYLE_CLASS = "m3-switch";
+
+ /// The default minimum touch target height.
+ private static final double DEFAULT_TOUCH_TARGET_SIZE = 40.0;
+
+ /// The default rounded switch track radius.
+ private static final double DEFAULT_TRACK_SHAPE = 999.0;
+
+ /// The legacy knob radius that maps to the default M3 switch scale.
+ private static final double DEFAULT_SIZE = 8.0;
+
+ /// The styleable minimum touch target height.
+ private @Nullable StyleableDoubleProperty touchTargetSize;
+
+ /// The styleable switch track radius.
+ private @Nullable StyleableDoubleProperty trackShape;
+
+ /// The legacy styleable knob radius used as the switch scale.
+ private @Nullable StyleableDoubleProperty size;
+
+ /// Whether focus state layers should be hidden.
+ private @Nullable StyleableBooleanProperty disableVisualFocus;
- /**
- * {@inheritDoc}
- */
+ /// Whether switch animations should be disabled.
+ private @Nullable StyleableBooleanProperty disableAnimation;
+
+ /// Creates an empty switch.
public JFXToggleButton() {
initialize();
}
- /**
- * {@inheritDoc}
- */
- @Override
- protected Skin> createDefaultSkin() {
- return new JFXToggleButtonSkin(this);
+ /// Creates a switch with text.
+ public JFXToggleButton(String text) {
+ super(text);
+ initialize();
}
- private void initialize() {
- this.getStyleClass().add(DEFAULT_STYLE_CLASS);
- // it's up for the user to add this behavior
-// toggleColor.addListener((o, oldVal, newVal) -> {
-// // update line color in case not set by the user
-// if(newVal instanceof Color)
-// toggleLineColor.set(((Color)newVal).desaturate().desaturate().brighter());
-// });
+ /// Returns the preferred touch target size.
+ public final double getTouchTargetSize() {
+ return touchTargetSize == null ? DEFAULT_TOUCH_TARGET_SIZE : touchTargetSize.get();
}
- /***************************************************************************
- * *
- * styleable Properties *
- * *
- **************************************************************************/
-
- /**
- * Initialize the style class to 'jfx-toggle-button'.
- *
- * This is the selector class from which CSS can be used to style
- * this control.
- */
- private static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
-
- /**
- * default color used when the button is toggled
- */
- private final StyleableObjectProperty toggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.TOGGLE_COLOR,
- JFXToggleButton.this,
- "toggleColor",
- Color.valueOf(
- "#009688"));
-
- public Paint getToggleColor() {
- return toggleColor == null ? Color.valueOf("#009688") : toggleColor.get();
+ /// Sets the preferred touch target size.
+ public final void setTouchTargetSize(double touchTargetSize) {
+ touchTargetSizeProperty().set(nonNegative(touchTargetSize, "touchTargetSize"));
}
- public StyleableObjectProperty toggleColorProperty() {
- return this.toggleColor;
+ /// Returns the preferred touch target size property.
+ public final StyleableDoubleProperty touchTargetSizeProperty() {
+ if (touchTargetSize == null) {
+ touchTargetSize = new SimpleStyleableDoubleProperty(
+ StyleableProperties.TOUCH_TARGET_SIZE,
+ this,
+ "touchTargetSize",
+ DEFAULT_TOUCH_TARGET_SIZE
+ ) {
+ /// Applies updated layout metrics when the token changes.
+ @Override
+ protected void invalidated() {
+ set(nonNegative(get(), "touchTargetSize"));
+ updateMetrics();
+ }
+ };
+ }
+ return touchTargetSize;
}
- public void setToggleColor(Paint color) {
- this.toggleColor.set(color);
+ /// Returns the switch track shape radius.
+ public final double getTrackShape() {
+ return trackShape == null ? DEFAULT_TRACK_SHAPE : trackShape.get();
}
- /**
- * default color used when the button is not toggled
- */
- private StyleableObjectProperty untoggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNTOGGLE_COLOR,
- JFXToggleButton.this,
- "unToggleColor",
- Color.valueOf(
- "#FAFAFA"));
-
- public Paint getUnToggleColor() {
- return untoggleColor == null ? Color.valueOf("#FAFAFA") : untoggleColor.get();
+ /// Sets the switch track shape radius.
+ public final void setTrackShape(double trackShape) {
+ trackShapeProperty().set(nonNegative(trackShape, "trackShape"));
}
- public StyleableObjectProperty unToggleColorProperty() {
- return this.untoggleColor;
+ /// Returns the switch track shape radius property.
+ public final StyleableDoubleProperty trackShapeProperty() {
+ if (trackShape == null) {
+ trackShape = new SimpleStyleableDoubleProperty(
+ StyleableProperties.TRACK_SHAPE,
+ this,
+ "trackShape",
+ DEFAULT_TRACK_SHAPE
+ ) {
+ /// Validates updated track shape values.
+ @Override
+ protected void invalidated() {
+ set(nonNegative(get(), "trackShape"));
+ }
+ };
+ }
+ return trackShape;
}
- public void setUnToggleColor(Paint color) {
- this.untoggleColor.set(color);
+ /// Returns the legacy knob radius used to scale the switch.
+ public double getSize() {
+ return size == null ? DEFAULT_SIZE : size.get();
}
- /**
- * default line color used when the button is toggled
- */
- private final StyleableObjectProperty toggleLineColor = new SimpleStyleableObjectProperty<>(
- StyleableProperties.TOGGLE_LINE_COLOR,
- JFXToggleButton.this,
- "toggleLineColor",
- Color.valueOf("#77C2BB"));
-
- public Paint getToggleLineColor() {
- return toggleLineColor == null ? Color.valueOf("#77C2BB") : toggleLineColor.get();
+ /// Sets the legacy knob radius used to scale the switch.
+ public void setSize(double size) {
+ sizeProperty().set(nonNegative(size, "size"));
}
- public StyleableObjectProperty toggleLineColorProperty() {
- return this.toggleLineColor;
+ /// Returns the legacy knob radius property used to scale the switch.
+ public StyleableDoubleProperty sizeProperty() {
+ if (size == null) {
+ size = new SimpleStyleableDoubleProperty(
+ StyleableProperties.SIZE,
+ this,
+ "size",
+ DEFAULT_SIZE
+ ) {
+ /// Applies updated layout metrics when the legacy size changes.
+ @Override
+ protected void invalidated() {
+ set(nonNegative(get(), "size"));
+ updateMetrics();
+ }
+ };
+ }
+ return size;
}
- public void setToggleLineColor(Paint color) {
- this.toggleLineColor.set(color);
+ /// Returns the visual focus suppression property.
+ public final StyleableBooleanProperty disableVisualFocusProperty() {
+ if (disableVisualFocus == null) {
+ disableVisualFocus = new SimpleStyleableBooleanProperty(
+ StyleableProperties.DISABLE_VISUAL_FOCUS,
+ this,
+ "disableVisualFocus",
+ false
+ );
+ }
+ return disableVisualFocus;
}
- /**
- * default line color used when the button is not toggled
- */
- private final StyleableObjectProperty untoggleLineColor = new SimpleStyleableObjectProperty<>(
- StyleableProperties.UNTOGGLE_LINE_COLOR,
- JFXToggleButton.this,
- "unToggleLineColor",
- Color.valueOf("#999999"));
-
- public Paint getUnToggleLineColor() {
- return untoggleLineColor == null ? Color.valueOf("#999999") : untoggleLineColor.get();
+ /// Returns whether focus state layers are hidden.
+ public final Boolean isDisableVisualFocus() {
+ return disableVisualFocus != null && disableVisualFocus.get();
}
- public StyleableObjectProperty unToggleLineColorProperty() {
- return this.untoggleLineColor;
+ /// Sets whether focus state layers should be hidden.
+ public final void setDisableVisualFocus(Boolean disabled) {
+ disableVisualFocusProperty().set(Objects.requireNonNull(disabled, "disabled"));
}
- public void setUnToggleLineColor(Paint color) {
- this.untoggleLineColor.set(color);
+ /// Returns the animation suppression property.
+ public final StyleableBooleanProperty disableAnimationProperty() {
+ if (disableAnimation == null) {
+ disableAnimation = new SimpleStyleableBooleanProperty(
+ StyleableProperties.DISABLE_ANIMATION,
+ this,
+ "disableAnimation",
+ !AnimationUtils.isAnimationEnabled()
+ );
+ }
+ return disableAnimation;
}
- /**
- * Default size of the toggle button.
- */
- private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
- StyleableProperties.SIZE,
- JFXToggleButton.this,
- "size",
- 10.0);
-
- public double getSize() {
- return size.get();
+ /// Returns whether switch animations are disabled.
+ public final Boolean isDisableAnimation() {
+ return disableAnimation != null ? disableAnimation.get() : !AnimationUtils.isAnimationEnabled();
}
- public StyleableDoubleProperty sizeProperty() {
- return this.size;
+ /// Sets whether switch animations should be disabled.
+ public final void setDisableAnimation(Boolean disabled) {
+ disableAnimationProperty().set(Objects.requireNonNull(disabled, "disabled"));
}
- public void setSize(double size) {
- this.size.set(size);
+ /// Creates the default Material Design 3 switch skin.
+ @Override
+ protected Skin> createDefaultSkin() {
+ return new JFXToggleButtonSkin(this);
}
- /**
- * Disable the visual indicator for focus
- */
- private final StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_VISUAL_FOCUS,
- JFXToggleButton.this,
- "disableVisualFocus",
- false);
-
- public final StyleableBooleanProperty disableVisualFocusProperty() {
- return this.disableVisualFocus;
+ /// Returns the CSS metadata for this control class.
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.STYLEABLES;
}
- public final Boolean isDisableVisualFocus() {
- return disableVisualFocus != null && this.disableVisualFocusProperty().get();
+ /// Returns the CSS metadata for this control.
+ @Override
+ public List> getControlCssMetaData() {
+ return getClassCssMetaData();
}
- public final void setDisableVisualFocus(final Boolean disabled) {
- this.disableVisualFocusProperty().set(disabled);
+ /// Returns accessibility attributes for switch selection state.
+ @Override
+ public @Nullable Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+ Objects.requireNonNull(attribute, "attribute");
+ return switch (attribute) {
+ case SELECTED -> isSelected();
+ case TOGGLE_STATE -> isSelected()
+ ? AccessibleAttribute.ToggleState.CHECKED
+ : AccessibleAttribute.ToggleState.UNCHECKED;
+ default -> super.queryAccessibleAttribute(attribute, parameters);
+ };
}
+ /// Adds base style classes and switch defaults.
+ private void initialize() {
+ addStyleClass(DEFAULT_STYLE_CLASS);
+ addStyleClass(M3_STYLE_CLASS);
+ setAccessibleRole(AccessibleRole.CHECK_BOX);
+ setAlignment(Pos.CENTER_LEFT);
+ setFocusTraversable(true);
+ setMnemonicParsing(true);
+ updateMetrics();
+ }
- /**
- * disable animation on button action
- */
- private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
- JFXToggleButton.this,
- "disableAnimation",
- !AnimationUtils.isAnimationEnabled());
-
- public final StyleableBooleanProperty disableAnimationProperty() {
- return this.disableAnimation;
+ /// Adds a style class when it is not already present.
+ private void addStyleClass(String styleClass) {
+ List styleClasses = getStyleClass();
+ if (!styleClasses.contains(styleClass)) {
+ styleClasses.add(styleClass);
+ }
}
- public final Boolean isDisableAnimation() {
- return disableAnimation != null && this.disableAnimationProperty().get();
+ /// Applies size-related component tokens to JavaFX layout properties.
+ private void updateMetrics() {
+ double scaledTrackHeight = 32.0 * getSize() / DEFAULT_SIZE;
+ double size = Math.max(getTouchTargetSize(), scaledTrackHeight);
+ setMinHeight(size);
+ setPrefHeight(size);
}
- public final void setDisableAnimation(final Boolean disabled) {
- this.disableAnimationProperty().set(disabled);
+ /// Validates that a size token is not negative.
+ private static double nonNegative(double value, String name) {
+ if (value < 0.0) {
+ throw new IllegalArgumentException(name + " must not be negative");
+ }
+ return value;
}
+ /// CSS metadata for switch component tokens.
+ @NotNullByDefault
private static final class StyleableProperties {
- private static final CssMetaData TOGGLE_COLOR =
- new CssMetaData<>("-jfx-toggle-color",
- PaintConverter.getInstance(), Color.valueOf("#009688")) {
- @Override
- public boolean isSettable(JFXToggleButton control) {
- return control.toggleColor == null || !control.toggleColor.isBound();
- }
-
- @Override
- public StyleableProperty getStyleableProperty(JFXToggleButton control) {
- return control.toggleColorProperty();
- }
- };
-
- private static final CssMetaData UNTOGGLE_COLOR =
- new CssMetaData<>("-jfx-untoggle-color",
- PaintConverter.getInstance(), Color.valueOf("#FAFAFA")) {
+ /// CSS metadata for the touch target size token.
+ private static final CssMetaData TOUCH_TARGET_SIZE =
+ new CssMetaData<>("-m3-touch-target-size", SizeConverter.getInstance(), DEFAULT_TOUCH_TARGET_SIZE) {
+ /// Returns whether this property can be set by CSS.
@Override
public boolean isSettable(JFXToggleButton control) {
- return control.untoggleColor == null || !control.untoggleColor.isBound();
+ return control.touchTargetSize == null || !control.touchTargetSize.isBound();
}
+ /// Returns the styleable property for a control.
@Override
- public StyleableProperty getStyleableProperty(JFXToggleButton control) {
- return control.unToggleColorProperty();
- }
- };
-
- private static final CssMetaData TOGGLE_LINE_COLOR =
- new CssMetaData<>("-jfx-toggle-line-color",
- PaintConverter.getInstance(), Color.valueOf("#77C2BB")) {
- @Override
- public boolean isSettable(JFXToggleButton control) {
- return control.toggleLineColor == null || !control.toggleLineColor.isBound();
- }
-
- @Override
- public StyleableProperty getStyleableProperty(JFXToggleButton control) {
- return control.toggleLineColorProperty();
+ public StyleableProperty getStyleableProperty(JFXToggleButton control) {
+ return control.touchTargetSizeProperty();
}
};
- private static final CssMetaData UNTOGGLE_LINE_COLOR =
- new CssMetaData<>("-jfx-untoggle-line-color",
- PaintConverter.getInstance(), Color.valueOf("#999999")) {
+ /// CSS metadata for the switch track shape token.
+ private static final CssMetaData TRACK_SHAPE =
+ new CssMetaData<>("-m3-track-shape", SizeConverter.getInstance(), DEFAULT_TRACK_SHAPE) {
+ /// Returns whether this property can be set by CSS.
@Override
public boolean isSettable(JFXToggleButton control) {
- return control.untoggleLineColor == null || !control.untoggleLineColor.isBound();
+ return control.trackShape == null || !control.trackShape.isBound();
}
+ /// Returns the styleable property for a control.
@Override
- public StyleableProperty getStyleableProperty(JFXToggleButton control) {
- return control.unToggleLineColorProperty();
+ public StyleableProperty getStyleableProperty(JFXToggleButton control) {
+ return control.trackShapeProperty();
}
};
+ /// CSS metadata for the legacy switch size token.
private static final CssMetaData SIZE =
- new CssMetaData<>("-jfx-size",
- StyleConverter.getSizeConverter(), 10.0) {
+ new CssMetaData<>("-jfx-size", SizeConverter.getInstance(), DEFAULT_SIZE) {
+ /// Returns whether this property can be set by CSS.
@Override
public boolean isSettable(JFXToggleButton control) {
- return !control.size.isBound();
+ return control.size == null || !control.size.isBound();
}
+ /// Returns the styleable property for a control.
@Override
public StyleableProperty getStyleableProperty(JFXToggleButton control) {
return control.sizeProperty();
}
};
+
+ /// CSS metadata for the visual focus suppression flag.
private static final CssMetaData DISABLE_VISUAL_FOCUS =
- new CssMetaData<>("-jfx-disable-visual-focus",
- BooleanConverter.getInstance(), false) {
+ new CssMetaData<>("-jfx-disable-visual-focus", BooleanConverter.getInstance(), false) {
+ /// Returns whether this property can be set by CSS.
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
}
+ /// Returns the styleable property for a control.
@Override
- public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
+ public StyleableProperty getStyleableProperty(JFXToggleButton control) {
return control.disableVisualFocusProperty();
}
};
+ /// CSS metadata for the animation suppression flag.
private static final CssMetaData DISABLE_ANIMATION =
- new CssMetaData<>("-jfx-disable-animation",
- BooleanConverter.getInstance(), false) {
+ new CssMetaData<>("-jfx-disable-animation", BooleanConverter.getInstance(), false) {
+ /// Returns whether this property can be set by CSS.
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
+ /// Returns the styleable property for a control.
@Override
- public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
+ public StyleableProperty getStyleableProperty(JFXToggleButton control) {
return control.disableAnimationProperty();
}
};
- private static final List> CHILD_STYLEABLES;
+ /// The complete immutable CSS metadata list.
+ private static final List> STYLEABLES;
static {
- final List> styleables =
- new ArrayList<>(ToggleButton.getClassCssMetaData());
- Collections.addAll(styleables,
+ List> styleables = new ArrayList<>(ToggleButton.getClassCssMetaData());
+ Collections.addAll(
+ styleables,
+ TOUCH_TARGET_SIZE,
+ TRACK_SHAPE,
SIZE,
- TOGGLE_COLOR,
- UNTOGGLE_COLOR,
- TOGGLE_LINE_COLOR,
- UNTOGGLE_LINE_COLOR,
DISABLE_VISUAL_FOCUS,
DISABLE_ANIMATION
);
- CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
+ STYLEABLES = Collections.unmodifiableList(styleables);
}
}
-
- @Override
- public List> getControlCssMetaData() {
- return getClassCssMetaData();
- }
-
- public static List> getClassCssMetaData() {
- return StyleableProperties.CHILD_STYLEABLES;
- }
-
}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXToggleButtonSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXToggleButtonSkin.java
index 0548204d7cb..95beb51e97b 100644
--- a/HMCL/src/main/java/com/jfoenix/skins/JFXToggleButtonSkin.java
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXToggleButtonSkin.java
@@ -19,168 +19,587 @@
package com.jfoenix.skins;
-import com.jfoenix.controls.JFXRippler;
-import com.jfoenix.controls.JFXRippler.RipplerMask;
-import com.jfoenix.controls.JFXRippler.RipplerPos;
import com.jfoenix.controls.JFXToggleButton;
-import com.jfoenix.effects.JFXDepthManager;
-import com.jfoenix.transitions.JFXAnimationTimer;
-import com.jfoenix.transitions.JFXKeyFrame;
-import com.jfoenix.transitions.JFXKeyValue;
import javafx.animation.Interpolator;
-import javafx.geometry.Insets;
-import javafx.scene.Cursor;
-import javafx.scene.control.skin.ToggleButtonSkin;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.InvalidationListener;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.event.EventHandler;
+import javafx.geometry.Point2D;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
-import javafx.scene.shape.Circle;
-import javafx.scene.shape.Line;
-import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
+import org.jetbrains.annotations.NotNullByDefault;
-/**
- * Material Design ToggleButton Skin
- *
- * @author Shadi Shaheen
- * @version 1.0
- * @since 2016-03-09
- */
-public class JFXToggleButtonSkin extends ToggleButtonSkin {
-
-
- private Runnable releaseManualRippler = null;
-
- private JFXAnimationTimer timer;
- private final Circle circle;
- private final Line line;
-
- public JFXToggleButtonSkin(JFXToggleButton toggleButton) {
- super(toggleButton);
-
- double circleRadius = toggleButton.getSize();
-
- line = new Line();
- line.setStroke(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
- line.setStartX(0);
- line.setStartY(0);
- line.setEndX(circleRadius * 2 + 2);
- line.setEndY(0);
- line.setStrokeWidth(circleRadius * 1.5);
- line.setStrokeLineCap(StrokeLineCap.ROUND);
- line.setSmooth(true);
-
- circle = new Circle();
- circle.setFill(getSkinnable().isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
- circle.setCenterX(-circleRadius);
- circle.setCenterY(0);
- circle.setRadius(circleRadius);
- circle.setSmooth(true);
- JFXDepthManager.setDepth(circle, 1);
-
- StackPane circlePane = new StackPane();
- circlePane.getChildren().add(circle);
- circlePane.setPadding(new Insets(circleRadius * 1.5));
-
- JFXRippler rippler = new JFXRippler(circlePane, RipplerMask.CIRCLE, RipplerPos.BACK);
- rippler.setRipplerFill(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
- rippler.setTranslateX(computeTranslation(circleRadius, line));
-
- final StackPane main = new StackPane();
- main.getChildren().setAll(line, rippler);
- main.setCursor(Cursor.HAND);
-
- // show focus traversal effect
- getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
- if (newVal) {
- releaseManualRippler = rippler.createManualRipple();
- } else if (releaseManualRippler != null) {
- releaseManualRippler.run();
- }
- });
- toggleButton.focusedProperty().addListener((o, oldVal, newVal) -> {
- if (!toggleButton.isDisableVisualFocus()) {
- if (newVal) {
- if (!getSkinnable().isPressed()) {
- rippler.setOverlayVisible(true);
- }
- } else {
- rippler.setOverlayVisible(false);
- }
- }
- });
- toggleButton.pressedProperty().addListener(observable -> rippler.setOverlayVisible(false));
-
- // add change listener to selected property
- getSkinnable().selectedProperty().addListener(observable -> {
- rippler.setRipplerFill(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
- if (!toggleButton.isDisableAnimation()) {
- timer.reverseAndContinue();
- } else {
- rippler.setTranslateX(computeTranslation(circleRadius, line));
- }
- });
-
- getSkinnable().setGraphic(main);
-
- timer = new JFXAnimationTimer(
- new JFXKeyFrame(Duration.millis(100),
- JFXKeyValue.builder()
- .setTarget(rippler.translateXProperty())
- .setEndValueSupplier(() -> computeTranslation(circleRadius, line))
- .setInterpolator(Interpolator.EASE_BOTH)
- .setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
- .build(),
-
- JFXKeyValue.builder()
- .setTarget(line.strokeProperty())
- .setEndValueSupplier(() -> getSkinnable().isSelected() ?
- ((JFXToggleButton) getSkinnable()).getToggleLineColor()
- : ((JFXToggleButton) getSkinnable()).getUnToggleLineColor())
- .setInterpolator(Interpolator.EASE_BOTH)
- .setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
- .build(),
-
- JFXKeyValue.builder()
- .setTarget(circle.fillProperty())
- .setEndValueSupplier(() -> getSkinnable().isSelected() ?
- ((JFXToggleButton) getSkinnable()).getToggleColor()
- : ((JFXToggleButton) getSkinnable()).getUnToggleColor())
- .setInterpolator(Interpolator.EASE_BOTH)
- .setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
- .build()
- )
- );
- timer.setCacheNodes(circle, line);
-
- registerChangeListener(toggleButton.toggleColorProperty(), observableValue -> {
- if (getSkinnable().isSelected()) {
- circle.setFill(((JFXToggleButton) getSkinnable()).getToggleColor());
- }
- });
- registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
- if (!getSkinnable().isSelected()) {
- circle.setFill(((JFXToggleButton) getSkinnable()).getUnToggleColor());
- }
- });
- registerChangeListener(toggleButton.toggleLineColorProperty(), observableValue -> {
- if (getSkinnable().isSelected()) {
- line.setStroke(((JFXToggleButton) getSkinnable()).getToggleLineColor());
- }
- });
- registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
- if (!getSkinnable().isSelected()) {
- line.setStroke(((JFXToggleButton) getSkinnable()).getUnToggleLineColor());
- }
- });
- }
+/// The Material Design 3 switch skin used by [JFXToggleButton].
+@NotNullByDefault
+public class JFXToggleButtonSkin extends SkinBase {
+ /// The unscaled switch track width.
+ private static final double TRACK_WIDTH = 52.0;
+
+ /// The unscaled switch track height.
+ private static final double TRACK_HEIGHT = 32.0;
+
+ /// The unscaled minimum circular state layer size around the thumb.
+ private static final double STATE_LAYER_SIZE = 40.0;
+
+ /// The unscaled off-state thumb center within the track.
+ private static final double OFF_THUMB_CENTER_X = 16.0;
+
+ /// The unscaled on-state thumb center within the track.
+ private static final double ON_THUMB_CENTER_X = 36.0;
+
+ /// The unscaled off-state thumb size.
+ private static final double OFF_THUMB_SIZE = 16.0;
+
+ /// The unscaled on-state thumb size.
+ private static final double ON_THUMB_SIZE = 24.0;
+
+ /// The legacy size value that represents the default M3 switch scale.
+ private static final double DEFAULT_SIZE = 8.0;
+
+ /// The animation duration for thumb movement.
+ private static final Duration SELECTION_DURATION = Duration.millis(150.0);
+
+ /// The animation duration for state layer opacity changes.
+ private static final Duration STATE_LAYER_DURATION = Duration.millis(100.0);
+
+ /// The animation duration for ripple expansion.
+ private static final Duration RIPPLE_EXPANSION_DURATION = Duration.millis(250.0);
+
+ /// The animation duration for ripple fade-out.
+ private static final Duration RIPPLE_FADE_DURATION = Duration.millis(150.0);
+
+ /// The root layout container.
+ private final HBox container = new HBox();
+
+ /// The switch touch target slot.
+ private final StackPane indicatorSlot = new StackPane();
+
+ /// The visual switch track.
+ private final StackPane box = new StackPane();
+
+ /// The visual switch thumb.
+ private final StackPane thumb = new StackPane();
+
+ /// The persistent interaction state layer.
+ private final Region stateLayer = new Region();
+
+ /// The animated press ripple.
+ private final Region ripple = new Region();
+
+ /// The label that mirrors the skinnable control's labeled content.
+ private final Label label = new Label();
+
+ /// The animated thumb position from off to on.
+ private final DoubleProperty thumbPosition = new SimpleDoubleProperty(this, "thumbPosition");
+
+ /// The thumb position animation.
+ private final Timeline selectionAnimation = new Timeline();
+
+ /// The state layer opacity animation.
+ private final Timeline stateLayerAnimation = new Timeline();
+
+ /// The ripple animation.
+ private final Timeline rippleAnimation = new Timeline();
+
+ /// Requests layout after thumb position changes.
+ private final InvalidationListener thumbPositionListener = observable -> getSkinnable().requestLayout();
- private double computeTranslation(double circleRadius, Line line) {
- return (getSkinnable().isSelected() ? 1 : -1) * ((line.getLayoutBounds().getWidth() / 2) - circleRadius + 2);
+ /// Applies size token changes to the switch layout.
+ private final InvalidationListener metricsInvalidation = observable -> updateMetrics();
+
+ /// Applies track shape token changes to the switch track.
+ private final InvalidationListener trackShapeInvalidation = observable -> updateTrackStyle();
+
+ /// Animates the thumb after selection changes.
+ private final ChangeListener selectedListener =
+ (observable, oldValue, newValue) -> animateThumbPosition(newValue);
+
+ /// Updates state layer opacity when an interaction state changes.
+ private final ChangeListener interactionStateListener =
+ (observable, oldValue, newValue) -> updateStateLayerOpacity();
+
+ /// Handles primary mouse presses.
+ private final EventHandler mousePressedHandler = this::handleMousePressed;
+
+ /// Handles primary mouse releases.
+ private final EventHandler mouseReleasedHandler = this::handleMouseReleased;
+
+ /// Handles pointer entry while a mouse press is active.
+ private final EventHandler mouseEnteredHandler = this::handleMouseEntered;
+
+ /// Handles pointer exit while a mouse press is active.
+ private final EventHandler mouseExitedHandler = this::handleMouseExited;
+
+ /// Handles keyboard activation presses.
+ private final EventHandler keyPressedHandler = this::handleKeyPressed;
+
+ /// Handles keyboard activation releases.
+ private final EventHandler keyReleasedHandler = this::handleKeyReleased;
+
+ /// Whether the current interaction was started by a primary mouse press.
+ private boolean mousePressed;
+
+ /// Whether the space key currently owns the armed state.
+ private boolean spaceKeyPressed;
+
+ /// Creates a switch skin.
+ public JFXToggleButtonSkin(JFXToggleButton control) {
+ super(control);
+ configureNodes(control);
+ bindLabel(control);
+ installListeners(control);
+ installInteractionHandlers(control);
+ thumbPosition.set(control.isSelected() ? 1.0 : 0.0);
+ thumbPosition.addListener(thumbPositionListener);
+ updateMetrics();
+ updateStateLayerOpacity();
}
+ /// Removes listeners and stops animations before the skin is disposed.
@Override
public void dispose() {
+ JFXToggleButton control = getSkinnable();
+ resetInteractionState();
+ selectionAnimation.stop();
+ stateLayerAnimation.stop();
+ rippleAnimation.stop();
+ thumbPosition.removeListener(thumbPositionListener);
+ uninstallListeners(control);
+ uninstallInteractionHandlers(control);
+ unbindLabel();
super.dispose();
- timer.dispose();
- timer = null;
+ }
+
+ /// Computes the minimum width from the internal container.
+ @Override
+ protected double computeMinWidth(
+ double height,
+ double topInset,
+ double rightInset,
+ double bottomInset,
+ double leftInset
+ ) {
+ return leftInset + container.minWidth(height) + rightInset;
+ }
+
+ /// Computes the minimum height from the internal container.
+ @Override
+ protected double computeMinHeight(
+ double width,
+ double topInset,
+ double rightInset,
+ double bottomInset,
+ double leftInset
+ ) {
+ return topInset + container.minHeight(width) + bottomInset;
+ }
+
+ /// Computes the preferred width from the internal container.
+ @Override
+ protected double computePrefWidth(
+ double height,
+ double topInset,
+ double rightInset,
+ double bottomInset,
+ double leftInset
+ ) {
+ return leftInset + container.prefWidth(height) + rightInset;
+ }
+
+ /// Computes the preferred height from the internal container.
+ @Override
+ protected double computePrefHeight(
+ double width,
+ double topInset,
+ double rightInset,
+ double bottomInset,
+ double leftInset
+ ) {
+ return topInset + container.prefHeight(width) + bottomInset;
+ }
+
+ /// Lays out the internal container and switch thumb.
+ @Override
+ protected void layoutChildren(double x, double y, double width, double height) {
+ container.resizeRelocate(x, y, width, height);
+ layoutThumb();
+ }
+
+ /// Configures static node classes and hierarchy.
+ private void configureNodes(JFXToggleButton control) {
+ container.getStyleClass().add("m3-selection-container");
+ indicatorSlot.getStyleClass().add("m3-selection-indicator");
+ box.getStyleClass().addAll("box", "m3-switch-track");
+ thumb.getStyleClass().addAll("thumb", "m3-switch-thumb");
+ stateLayer.getStyleClass().add("m3-state-layer");
+ ripple.getStyleClass().add("m3-ripple");
+ label.getStyleClass().add("m3-selection-label");
+
+ container.setAlignment(Pos.CENTER_LEFT);
+ indicatorSlot.setAlignment(Pos.CENTER);
+ stateLayer.setManaged(false);
+ ripple.setManaged(false);
+ thumb.setManaged(false);
+ stateLayer.setMouseTransparent(true);
+ ripple.setMouseTransparent(true);
+ stateLayer.setOpacity(0.0);
+ ripple.setOpacity(0.0);
+
+ indicatorSlot.getChildren().addAll(box, stateLayer, ripple, thumb);
+ container.getChildren().addAll(indicatorSlot, label);
+ getChildren().add(container);
+
+ }
+
+ /// Installs property listeners for layout, state, and interaction updates.
+ private void installListeners(JFXToggleButton control) {
+ control.sizeProperty().addListener(metricsInvalidation);
+ control.touchTargetSizeProperty().addListener(metricsInvalidation);
+ control.trackShapeProperty().addListener(trackShapeInvalidation);
+ control.selectedProperty().addListener(selectedListener);
+ control.hoverProperty().addListener(interactionStateListener);
+ control.focusedProperty().addListener(interactionStateListener);
+ control.armedProperty().addListener(interactionStateListener);
+ control.pressedProperty().addListener(interactionStateListener);
+ control.disabledProperty().addListener(interactionStateListener);
+ }
+
+ /// Uninstalls property listeners installed by this skin.
+ private void uninstallListeners(JFXToggleButton control) {
+ control.sizeProperty().removeListener(metricsInvalidation);
+ control.touchTargetSizeProperty().removeListener(metricsInvalidation);
+ control.trackShapeProperty().removeListener(trackShapeInvalidation);
+ control.selectedProperty().removeListener(selectedListener);
+ control.hoverProperty().removeListener(interactionStateListener);
+ control.focusedProperty().removeListener(interactionStateListener);
+ control.armedProperty().removeListener(interactionStateListener);
+ control.pressedProperty().removeListener(interactionStateListener);
+ control.disabledProperty().removeListener(interactionStateListener);
+ }
+
+ /// Installs mouse and keyboard behavior handlers.
+ private void installInteractionHandlers(JFXToggleButton control) {
+ control.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedHandler);
+ control.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler);
+ control.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredHandler);
+ control.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
+ control.addEventHandler(KeyEvent.KEY_PRESSED, keyPressedHandler);
+ control.addEventHandler(KeyEvent.KEY_RELEASED, keyReleasedHandler);
+ }
+
+ /// Uninstalls mouse and keyboard behavior handlers.
+ private void uninstallInteractionHandlers(JFXToggleButton control) {
+ control.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedHandler);
+ control.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedHandler);
+ control.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredHandler);
+ control.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedHandler);
+ control.removeEventHandler(KeyEvent.KEY_PRESSED, keyPressedHandler);
+ control.removeEventHandler(KeyEvent.KEY_RELEASED, keyReleasedHandler);
+ }
+
+ /// Binds label content and presentation properties to the skinnable control.
+ private void bindLabel(JFXToggleButton control) {
+ label.textProperty().bind(control.textProperty());
+ label.graphicProperty().bind(control.graphicProperty());
+ label.textFillProperty().bind(control.textFillProperty());
+ label.fontProperty().bind(control.fontProperty());
+ label.contentDisplayProperty().bind(control.contentDisplayProperty());
+ label.graphicTextGapProperty().bind(control.graphicTextGapProperty());
+ label.alignmentProperty().bind(control.alignmentProperty());
+ label.textAlignmentProperty().bind(control.textAlignmentProperty());
+ label.textOverrunProperty().bind(control.textOverrunProperty());
+ label.ellipsisStringProperty().bind(control.ellipsisStringProperty());
+ label.wrapTextProperty().bind(control.wrapTextProperty());
+ label.underlineProperty().bind(control.underlineProperty());
+ label.mnemonicParsingProperty().bind(control.mnemonicParsingProperty());
+ }
+
+ /// Unbinds mirrored label properties from the skinnable control.
+ private void unbindLabel() {
+ label.textProperty().unbind();
+ label.graphicProperty().unbind();
+ label.textFillProperty().unbind();
+ label.fontProperty().unbind();
+ label.contentDisplayProperty().unbind();
+ label.graphicTextGapProperty().unbind();
+ label.alignmentProperty().unbind();
+ label.textAlignmentProperty().unbind();
+ label.textOverrunProperty().unbind();
+ label.ellipsisStringProperty().unbind();
+ label.wrapTextProperty().unbind();
+ label.underlineProperty().unbind();
+ label.mnemonicParsingProperty().unbind();
+ }
+
+ /// Applies size-related control tokens to the skin nodes.
+ private void updateMetrics() {
+ double scale = scale();
+ double trackWidth = TRACK_WIDTH * scale;
+ double trackHeight = TRACK_HEIGHT * scale;
+ double touchTargetHeight = Math.max(getSkinnable().getTouchTargetSize(), trackHeight);
+ setFixedSize(indicatorSlot, trackWidth, touchTargetHeight);
+ setFixedSize(box, trackWidth, trackHeight);
+ updateTrackStyle();
+ getSkinnable().requestLayout();
+ }
+
+ /// Applies the switch track shape token to the visual track.
+ private void updateTrackStyle() {
+ String shape = formatPixels(getSkinnable().getTrackShape());
+ box.setStyle("-fx-background-radius: " + shape + "; -fx-border-radius: " + shape + ";");
+ }
+
+ /// Animates the thumb to the selected or unselected position.
+ private void animateThumbPosition(boolean selected) {
+ double target = selected ? 1.0 : 0.0;
+ selectionAnimation.stop();
+ if (getSkinnable().isDisableAnimation()) {
+ thumbPosition.set(target);
+ return;
+ }
+
+ selectionAnimation.getKeyFrames().setAll(new KeyFrame(
+ SELECTION_DURATION,
+ new KeyValue(thumbPosition, target, Interpolator.EASE_BOTH)
+ ));
+ selectionAnimation.playFromStart();
+ }
+
+ /// Lays out the thumb and its interaction layers from the animated position value.
+ private void layoutThumb() {
+ double scale = scale();
+ double position = thumbPosition.get();
+ double thumbSize = scaled(OFF_THUMB_SIZE + (ON_THUMB_SIZE - OFF_THUMB_SIZE) * position, scale);
+ double thumbCenterX = scaled(OFF_THUMB_CENTER_X + (ON_THUMB_CENTER_X - OFF_THUMB_CENTER_X) * position, scale);
+ double thumbX = thumbCenterX - thumbSize / 2.0;
+ double touchTargetHeight = Math.max(getSkinnable().getTouchTargetSize(), scaled(TRACK_HEIGHT, scale));
+ double thumbY = (touchTargetHeight - thumbSize) / 2.0;
+ double stateLayerSize = Math.max(scaled(STATE_LAYER_SIZE, scale), getSkinnable().getTouchTargetSize());
+ double stateLayerX = thumbCenterX - stateLayerSize / 2.0;
+ double stateLayerY = (touchTargetHeight - stateLayerSize) / 2.0;
+ double radius = stateLayerSize / 2.0;
+
+ layoutStateRegion(stateLayer, stateLayerX, stateLayerY, stateLayerSize, radius);
+ layoutStateRegion(ripple, stateLayerX, stateLayerY, stateLayerSize, radius);
+ thumb.resizeRelocate(thumbX, thumbY, thumbSize, thumbSize);
+ }
+
+ /// Applies a circular size and radius to a state feedback region.
+ private void layoutStateRegion(Region region, double x, double y, double size, double radius) {
+ region.resizeRelocate(x, y, size, size);
+ region.setStyle("-fx-background-radius: " + formatPixels(radius) + ";");
+ }
+
+ /// Updates the persistent state layer opacity from current interaction state.
+ private void updateStateLayerOpacity() {
+ JFXToggleButton control = getSkinnable();
+ double targetOpacity;
+ if (control.isDisabled()) {
+ targetOpacity = 0.0;
+ } else if (control.isPressed() || control.isArmed()) {
+ targetOpacity = 0.12;
+ } else if (control.isFocused() && !control.isDisableVisualFocus()) {
+ targetOpacity = 0.10;
+ } else if (control.isHover()) {
+ targetOpacity = 0.08;
+ } else {
+ targetOpacity = 0.0;
+ }
+
+ animateOpacity(stateLayer, stateLayerAnimation, targetOpacity, STATE_LAYER_DURATION);
+ }
+
+ /// Arms the control on primary mouse press.
+ private void handleMousePressed(MouseEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (control.isDisabled() || event.getButton() != MouseButton.PRIMARY) {
+ return;
+ }
+
+ mousePressed = true;
+ if (control.isFocusTraversable()) {
+ control.requestFocus();
+ }
+ playRipple(event);
+ control.arm();
+ event.consume();
+ }
+
+ /// Fires the control when a primary mouse press is released inside the control.
+ private void handleMouseReleased(MouseEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (!mousePressed || event.getButton() != MouseButton.PRIMARY) {
+ return;
+ }
+
+ boolean shouldFire = control.isArmed() && control.contains(event.getX(), event.getY());
+ mousePressed = false;
+ releaseRipple();
+ control.disarm();
+ if (shouldFire) {
+ control.fire();
+ }
+ event.consume();
+ }
+
+ /// Re-arms the control when a pressed pointer re-enters the control.
+ private void handleMouseEntered(MouseEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (mousePressed && !control.isDisabled()) {
+ control.arm();
+ event.consume();
+ }
+ }
+
+ /// Disarms the control when a pressed pointer exits the control.
+ private void handleMouseExited(MouseEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (mousePressed && !control.isDisabled()) {
+ control.disarm();
+ event.consume();
+ }
+ }
+
+ /// Handles keyboard activation for enter and space.
+ private void handleKeyPressed(KeyEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (control.isDisabled()) {
+ return;
+ }
+
+ if (event.getCode() == KeyCode.SPACE) {
+ if (!spaceKeyPressed) {
+ spaceKeyPressed = true;
+ playCenteredRipple();
+ control.arm();
+ }
+ event.consume();
+ } else if (event.getCode() == KeyCode.ENTER) {
+ playCenteredRipple();
+ releaseRipple();
+ control.fire();
+ event.consume();
+ }
+ }
+
+ /// Fires the control when a space key activation is released.
+ private void handleKeyReleased(KeyEvent event) {
+ JFXToggleButton control = getSkinnable();
+ if (event.getCode() != KeyCode.SPACE || !spaceKeyPressed) {
+ return;
+ }
+
+ boolean shouldFire = control.isArmed() && !control.isDisabled();
+ spaceKeyPressed = false;
+ releaseRipple();
+ control.disarm();
+ if (shouldFire) {
+ control.fire();
+ }
+ event.consume();
+ }
+
+ /// Clears armed state and transient feedback.
+ private void resetInteractionState() {
+ mousePressed = false;
+ spaceKeyPressed = false;
+ rippleAnimation.stop();
+ ripple.setOpacity(0.0);
+ getSkinnable().disarm();
+ }
+
+ /// Plays the press ripple from a mouse event.
+ private void playRipple(MouseEvent event) {
+ Point2D point = indicatorSlot.sceneToLocal(event.getSceneX(), event.getSceneY());
+ ripple.setTranslateX((point.getX() - (ripple.getLayoutX() + ripple.getWidth() / 2.0)) * 0.12);
+ ripple.setTranslateY((point.getY() - (ripple.getLayoutY() + ripple.getHeight() / 2.0)) * 0.12);
+ playRippleAnimation();
+ }
+
+ /// Plays the press ripple from the current thumb center.
+ private void playCenteredRipple() {
+ ripple.setTranslateX(0.0);
+ ripple.setTranslateY(0.0);
+ playRippleAnimation();
+ }
+
+ /// Starts the ripple expansion animation.
+ private void playRippleAnimation() {
+ rippleAnimation.stop();
+ ripple.setScaleX(0.45);
+ ripple.setScaleY(0.45);
+ ripple.setOpacity(0.18);
+ if (getSkinnable().isDisableAnimation()) {
+ ripple.setScaleX(1.0);
+ ripple.setScaleY(1.0);
+ return;
+ }
+
+ rippleAnimation.getKeyFrames().setAll(new KeyFrame(
+ RIPPLE_EXPANSION_DURATION,
+ new KeyValue(ripple.scaleXProperty(), 1.0, Interpolator.EASE_OUT),
+ new KeyValue(ripple.scaleYProperty(), 1.0, Interpolator.EASE_OUT)
+ ));
+ rippleAnimation.playFromStart();
+ }
+
+ /// Releases the active ripple and fades it out.
+ private void releaseRipple() {
+ rippleAnimation.stop();
+ animateOpacity(ripple, rippleAnimation, 0.0, RIPPLE_FADE_DURATION);
+ }
+
+ /// Animates a region opacity or applies the target immediately when animations are disabled.
+ private void animateOpacity(Region region, Timeline timeline, double targetOpacity, Duration duration) {
+ timeline.stop();
+ if (getSkinnable().isDisableAnimation()) {
+ region.setOpacity(targetOpacity);
+ return;
+ }
+
+ timeline.getKeyFrames().setAll(new KeyFrame(
+ duration,
+ new KeyValue(region.opacityProperty(), targetOpacity, Interpolator.EASE_BOTH)
+ ));
+ timeline.playFromStart();
+ }
+
+ /// Applies a fixed size to a region.
+ private static void setFixedSize(Region region, double width, double height) {
+ region.setMinSize(width, height);
+ region.setPrefSize(width, height);
+ region.setMaxSize(width, height);
+ }
+
+ /// Returns the current legacy-size scale factor.
+ private double scale() {
+ return getSkinnable().getSize() / DEFAULT_SIZE;
+ }
+
+ /// Scales a base pixel value.
+ private static double scaled(double value, double scale) {
+ return value * scale;
+ }
+
+ /// Formats a CSS pixel value.
+ private static String formatPixels(double value) {
+ if (Math.rint(value) == value) {
+ return (long) value + "px";
+ }
+ return value + "px";
}
}
diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css
index 26a9bccadd0..c69bcb343af 100644
--- a/HMCL/src/main/resources/assets/css/root.css
+++ b/HMCL/src/main/resources/assets/css/root.css
@@ -1180,40 +1180,99 @@
.jfx-toggle-button:focused,
.jfx-toggle-button:selected,
.jfx-toggle-button:focused:selected {
- -fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
+ -m3-touch-target-size: 40px;
+ -m3-track-shape: 999px;
+ -jfx-size: 8px;
+ -fx-background-color: transparent;
-fx-background-radius: 3px;
- -fx-background-insets: 0px;
+ -fx-background-insets: 0;
+ -fx-text-fill: -monet-on-surface;
+ -fx-font-size: 14px;
+}
- -jfx-toggle-color: -monet-primary;
- -jfx-toggle-line-color: -monet-secondary-container;
- -jfx-untoggle-color: -monet-outline;
- -jfx-untoggle-line-color: -monet-surface-container-highest;
- -jfx-size: 10;
+.jfx-toggle-button .m3-selection-container {
+ -fx-alignment: center-left;
+ -fx-spacing: 4px;
}
+.jfx-toggle-button .m3-selection-indicator {
+ -fx-alignment: center;
+}
-.jfx-toggle-button Line {
- -fx-stroke: -jfx-untoggle-line-color;
+.jfx-toggle-button .box,
+.jfx-toggle-button:hover .box,
+.jfx-toggle-button:focused .box,
+.jfx-toggle-button:armed .box,
+.jfx-toggle-button:pressed .box {
+ -fx-background-color: -monet-surface-container-highest;
+ -fx-background-radius: 999px;
+ -fx-border-color: -monet-outline;
+ -fx-border-insets: 0;
+ -fx-border-radius: 999px;
+ -fx-border-width: 2px;
+ -fx-padding: 4px;
+ -fx-alignment: center-left;
}
-.jfx-toggle-button:selected Line {
- -fx-stroke: -jfx-toggle-line-color;
+.jfx-toggle-button:selected .box,
+.jfx-toggle-button:selected:hover .box,
+.jfx-toggle-button:selected:focused .box,
+.jfx-toggle-button:selected:armed .box,
+.jfx-toggle-button:selected:pressed .box {
+ -fx-background-color: -monet-primary;
+ -fx-border-color: -monet-primary;
}
-.jfx-toggle-button Circle {
- -fx-fill: -jfx-untoggle-color;
+.jfx-toggle-button .thumb,
+.jfx-toggle-button:hover .thumb,
+.jfx-toggle-button:focused .thumb,
+.jfx-toggle-button:armed .thumb,
+.jfx-toggle-button:pressed .thumb {
+ -fx-background-color: -monet-outline;
+ -fx-background-insets: 0;
+ -fx-background-radius: 999px;
}
-.jfx-toggle-button:selected Circle {
- -fx-fill: -jfx-toggle-color;
+.jfx-toggle-button:selected .thumb,
+.jfx-toggle-button:selected:hover .thumb,
+.jfx-toggle-button:selected:focused .thumb,
+.jfx-toggle-button:selected:armed .thumb,
+.jfx-toggle-button:selected:pressed .thumb {
+ -fx-background-color: -monet-on-primary;
}
-.jfx-toggle-button .jfx-rippler {
- -jfx-rippler-fill: -jfx-untoggle-line-color;
+.jfx-toggle-button .m3-state-layer,
+.jfx-toggle-button .m3-ripple {
+ -fx-background-color: -monet-primary;
+ -fx-background-insets: 0;
+}
+
+.jfx-toggle-button:disabled .m3-selection-label {
+ -fx-opacity: 0.38;
}
-.jfx-toggle-button:selected .jfx-rippler {
- -jfx-rippler-fill: -jfx-toggle-line-color;
+.jfx-toggle-button:disabled .box,
+.jfx-toggle-button:disabled:hover .box,
+.jfx-toggle-button:disabled:focused .box,
+.jfx-toggle-button:disabled:armed .box,
+.jfx-toggle-button:disabled:pressed .box {
+ -fx-background-color: -monet-on-surface;
+ -fx-border-color: -monet-on-surface;
+ -fx-opacity: 0.12;
+}
+
+.jfx-toggle-button:disabled .thumb,
+.jfx-toggle-button:disabled:hover .thumb,
+.jfx-toggle-button:disabled:focused .thumb,
+.jfx-toggle-button:disabled:armed .thumb,
+.jfx-toggle-button:disabled:pressed .thumb,
+.jfx-toggle-button:selected:disabled .thumb,
+.jfx-toggle-button:selected:disabled:hover .thumb,
+.jfx-toggle-button:selected:disabled:focused .thumb,
+.jfx-toggle-button:selected:disabled:armed .thumb,
+.jfx-toggle-button:selected:disabled:pressed .thumb {
+ -fx-background-color: -monet-on-surface;
+ -fx-opacity: 0.38;
}