Browse Source

:boom: New control: MFXSlider (incomplete, almost finished)

:boom: Many additions and changes
:boom: Added new JavaFX properties which hold a default value and ca be reset to that value. They can also set to not fire a value changed event when reset

NumberRange:
:sparkles: New bean to represent a numbers range from min to max. JavaFX IndexRange is limited because it only allows integer values

NumberUtils:
:boom: New utility for working with numbers

AnimationUtils:
:sparkles: Added new convenience add() methods
:sparkles: PauseBuilder added new runWhile() methods

ExecutionUtils:
:sparkles: Added new convenience method

NodeUtils:
:recycle: Renamed getNodeHeight() and getNodeWidth(), for Regions
:sparkles: Added new getNodeWidth() and getNodeWidth(), for Nodes
:bug: getRegionHeight(), getRegionWidth(), clear the group children list to avoid JavaFX exception (node already preset in another scene)

:memo: NOTE!! Documentation for new methods and changes will be added soon, pushing this to staging as tomorrow I won't be able to work on my main pc
palexdev 4 years ago
parent
commit
71965f7d58
22 changed files with 2180 additions and 11 deletions
  1. 47 0
      demo/src/test/java/treeview/NumberUtilsTest.java
  2. 23 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/NumberRange.java
  3. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableBooleanProperty.java
  4. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableDoubleProperty.java
  5. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableFloatProperty.java
  6. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableIntegerProperty.java
  7. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableLongProperty.java
  8. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableObjectProperty.java
  9. 75 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableStringProperty.java
  10. 17 0
      materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/base/ResettableProperty.java
  11. 620 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXSlider.java
  12. 14 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SliderEnum.java
  13. 3 0
      materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/Styles.java
  14. 650 0
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXSliderSkin.java
  15. 2 2
      materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTreeItemSkin.java
  16. 68 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/AnimationUtils.java
  17. 21 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/ExecutionUtils.java
  18. 3 3
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/LabelUtils.java
  19. 44 6
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java
  20. 60 0
      materialfx/src/main/java/io/github/palexdev/materialfx/utils/NumberUtils.java
  21. 2 0
      materialfx/src/main/java/module-info.java
  22. 81 0
      materialfx/src/main/resources/io/github/palexdev/materialfx/css/MFXSlider.css

+ 47 - 0
demo/src/test/java/treeview/NumberUtilsTest.java

@@ -0,0 +1,47 @@
+package treeview;
+
+import io.github.palexdev.materialfx.beans.NumberRange;
+import io.github.palexdev.materialfx.utils.NumberUtils;
+import junit.framework.TestCase;
+
+public class NumberUtilsTest extends TestCase {
+    private double val;
+    private NumberRange<Double> fromRange;
+    private NumberRange<Double> toRange;
+
+    public void testMap1() {
+        val = 0;
+        fromRange = NumberRange.of(-50.0, 100.0);
+        toRange = NumberRange.of(0.0, 100.0);
+
+        double mapped = NumberUtils.mapOneRangeToAnother(val, fromRange, toRange, 1);
+        assertEquals(33.3, mapped);
+    }
+
+    public void testMap2() {
+        val = -50;
+        fromRange = NumberRange.of(-50.0, 100.0);
+        toRange = NumberRange.of(0.0, 100.0);
+
+        double mapped = NumberUtils.mapOneRangeToAnother(val, fromRange, toRange, 1);
+        assertEquals(0.0, mapped);
+    }
+
+    public void testMap3() {
+        val = 100;
+        fromRange = NumberRange.of(-50.0, 100.0);
+        toRange = NumberRange.of(0.0, 100.0);
+
+        double mapped = NumberUtils.mapOneRangeToAnother(val, fromRange, toRange, 1);
+        assertEquals(100.0, mapped);
+    }
+
+    public void testMap4() {
+        val = -10;
+        fromRange = NumberRange.of(-50.0, 100.0);
+        toRange = NumberRange.of(0.0, 100.0);
+
+        double mapped = NumberUtils.mapOneRangeToAnother(val, fromRange, toRange, 1);
+        assertEquals(26.7, mapped);
+    }
+}

+ 23 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/NumberRange.java

@@ -0,0 +1,23 @@
+package io.github.palexdev.materialfx.beans;
+
+public class NumberRange<T extends Number> {
+    private final T min;
+    private final T max;
+
+    public NumberRange(T min, T max) {
+        this.min = min;
+        this.max = max;
+    }
+
+    public T getMin() {
+        return min;
+    }
+
+    public T getMax() {
+        return max;
+    }
+
+    public static <T extends Number> NumberRange<T> of(T min, T max) {
+        return new NumberRange<>(min, max);
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableBooleanProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+
+public class ResettableBooleanProperty extends SimpleBooleanProperty implements ResettableProperty<Boolean> {
+    private boolean defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableBooleanProperty() {
+    }
+
+    public ResettableBooleanProperty(boolean initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableBooleanProperty(boolean initialValue, boolean defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableBooleanProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableBooleanProperty(Object bean, String name, boolean initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableBooleanProperty(Object bean, String name, boolean initialValue, boolean defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(boolean newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue() == defaultValue && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public Boolean getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(Boolean defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableDoubleProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+
+public class ResettableDoubleProperty extends SimpleDoubleProperty implements ResettableProperty<Number> {
+    private double defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableDoubleProperty() {
+    }
+
+    public ResettableDoubleProperty(double initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableDoubleProperty(double initialValue, double defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableDoubleProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableDoubleProperty(Object bean, String name, double initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableDoubleProperty(Object bean, String name, double initialValue, Double defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(double newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue() == defaultValue && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public Double getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(Number defaultValue) {
+        this.defaultValue = defaultValue.doubleValue();
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableFloatProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleFloatProperty;
+
+public class ResettableFloatProperty extends SimpleFloatProperty implements ResettableProperty<Number> {
+    private float defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableFloatProperty() {
+    }
+
+    public ResettableFloatProperty(float initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableFloatProperty(float initialValue, float defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableFloatProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableFloatProperty(Object bean, String name, float initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableFloatProperty(Object bean, String name, float initialValue, Float defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(float newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue() == defaultValue && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public Float getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(Number defaultValue) {
+        this.defaultValue = defaultValue.floatValue();
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableIntegerProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+
+public class ResettableIntegerProperty extends SimpleIntegerProperty implements ResettableProperty<Number> {
+    private int defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableIntegerProperty() {
+    }
+
+    public ResettableIntegerProperty(int initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableIntegerProperty(int initialValue, int defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableIntegerProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableIntegerProperty(Object bean, String name, int initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableIntegerProperty(Object bean, String name, int initialValue, int defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(int newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue() == defaultValue && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public Integer getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(Number defaultValue) {
+        this.defaultValue = defaultValue.intValue();
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableLongProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleLongProperty;
+
+public class ResettableLongProperty extends SimpleLongProperty implements ResettableProperty<Number> {
+    private long defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableLongProperty() {
+    }
+
+    public ResettableLongProperty(long initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableLongProperty(long initialValue, long defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableLongProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableLongProperty(Object bean, String name, long initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableLongProperty(Object bean, String name, long initialValue, long defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(long newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue() == defaultValue && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public Long getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(Number defaultValue) {
+        this.defaultValue = defaultValue.longValue();
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableObjectProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleObjectProperty;
+
+public class ResettableObjectProperty<T> extends SimpleObjectProperty<T> implements ResettableProperty<T> {
+    private T defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableObjectProperty() {
+    }
+
+    public ResettableObjectProperty(T initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableObjectProperty(T initialValue, T defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableObjectProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableObjectProperty(Object bean, String name, T initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableObjectProperty(Object bean, String name, T initialValue, T defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(T newValue) {
+        hasBeenReset = newValue == defaultValue;
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue().equals(defaultValue) && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public T getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(T defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+}

+ 75 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/ResettableStringProperty.java

@@ -0,0 +1,75 @@
+package io.github.palexdev.materialfx.beans.properties;
+
+import io.github.palexdev.materialfx.beans.properties.base.ResettableProperty;
+import javafx.beans.property.SimpleStringProperty;
+
+public class ResettableStringProperty extends SimpleStringProperty implements ResettableProperty<String> {
+    private String defaultValue;
+    private boolean fireChangeOnReset = false;
+    private boolean hasBeenReset = false;
+
+    public ResettableStringProperty() {
+    }
+
+    public ResettableStringProperty(String initialValue) {
+        super(initialValue);
+    }
+
+    public ResettableStringProperty(String initialValue, String defaultValue) {
+        super(initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    public ResettableStringProperty(Object bean, String name) {
+        super(bean, name);
+    }
+
+    public ResettableStringProperty(Object bean, String name, String initialValue) {
+        super(bean, name, initialValue);
+    }
+
+    public ResettableStringProperty(Object bean, String name, String initialValue, String defaultValue) {
+        super(bean, name, initialValue);
+        this.defaultValue = defaultValue;
+    }
+
+    @Override
+    public boolean isFireChangeOnReset() {
+        return fireChangeOnReset;
+    }
+
+    @Override
+    public void setFireChangeOnReset(boolean fireChangeOnReset) {
+        this.fireChangeOnReset = fireChangeOnReset;
+    }
+
+    @Override
+    public void set(String newValue) {
+        hasBeenReset = newValue.equals(defaultValue);
+        super.set(newValue);
+    }
+
+    @Override
+    protected void fireValueChangedEvent() {
+        if (getValue().equals(defaultValue) && !fireChangeOnReset) {
+            return;
+        }
+
+        super.fireValueChangedEvent();
+    }
+
+    @Override
+    public boolean hasBeenReset() {
+        return hasBeenReset;
+    }
+
+    @Override
+    public String getDefaultValue() {
+        return defaultValue;
+    }
+
+    @Override
+    public void setDefaultValue(String defaultValue) {
+        this.defaultValue = defaultValue;
+    }
+}

+ 17 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/beans/properties/base/ResettableProperty.java

@@ -0,0 +1,17 @@
+package io.github.palexdev.materialfx.beans.properties.base;
+
+import javafx.beans.property.Property;
+
+public interface ResettableProperty<T> extends Property<T> {
+    default void reset() {
+        setValue(getDefaultValue());
+    }
+
+
+
+    boolean isFireChangeOnReset();
+    void setFireChangeOnReset(boolean fireChangeOnReset);
+    boolean hasBeenReset();
+    T getDefaultValue();
+    void setDefaultValue(T defaultValue);
+}

+ 620 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/MFXSlider.java

@@ -0,0 +1,620 @@
+package io.github.palexdev.materialfx.controls;
+
+import io.github.palexdev.materialfx.MFXResourcesLoader;
+import io.github.palexdev.materialfx.controls.enums.SliderEnum.SliderMode;
+import io.github.palexdev.materialfx.controls.enums.SliderEnum.SliderPopupSide;
+import io.github.palexdev.materialfx.effects.ripple.MFXCircleRippleGenerator;
+import io.github.palexdev.materialfx.effects.ripple.RipplePosition;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.skins.MFXSliderSkin;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import io.github.palexdev.materialfx.utils.NumberUtils;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.*;
+import javafx.css.*;
+import javafx.event.Event;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Control;
+import javafx.scene.control.Label;
+import javafx.scene.control.Skin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.TextBoundsType;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public class MFXSlider extends Control {
+    //================================================================================
+    // Properties
+    //================================================================================
+    private static final StyleablePropertyFactory<MFXSlider> FACTORY = new StyleablePropertyFactory<>(Control.getClassCssMetaData());
+    private final String STYLE_CLASS = "mfx-slider";
+    private final String STYLESHEET = MFXResourcesLoader.load("css/MFXSlider.css");
+
+    private final DoubleProperty min = new SimpleDoubleProperty() {
+        @Override
+        public void set(double newValue) {
+            if (newValue > getMax()) {
+                throw new IllegalArgumentException("The minimum value cannot be greater than the max value");
+            }
+            super.set(newValue);
+        }
+    };
+    private final DoubleProperty max = new SimpleDoubleProperty() {
+        @Override
+        public void set(double newValue) {
+            if (newValue < getMin()) {
+                throw new IllegalArgumentException("The maximum value cannot be lesser than the min value");
+            }
+            super.set(newValue);
+        }
+    };
+    private final DoubleProperty value = new SimpleDoubleProperty() {
+        @Override
+        public void set(double newValue) {
+            double clamped = NumberUtils.clamp(newValue, getMin(), getMax());
+            super.set(NumberUtils.formatTo(clamped, getDecimalPrecision()));
+        }
+    };
+    private final ObjectProperty<Supplier<Node>> thumbSupplier = new SimpleObjectProperty<>() {
+        @Override
+        public void set(Supplier<Node> newValue) {
+            Node node = newValue.get();
+            if (node != null) {
+                super.set(newValue);
+            } else {
+                throw new NullPointerException("Thumb supplier not set as the return values was null!");
+            }
+        }
+    };
+    private final ObjectProperty<Supplier<Region>> popupSupplier = new SimpleObjectProperty<>();
+    private final IntegerProperty decimalPrecision = new SimpleIntegerProperty(2);
+
+    //================================================================================
+    // Constructors
+    //================================================================================
+
+    public MFXSlider() {
+        this(0);
+    }
+
+    public MFXSlider(double initialValue) {
+        this(0, 100, initialValue);
+    }
+
+    public MFXSlider(double min, double max, double initialValue) {
+        if (min > max) {
+            throw new IllegalArgumentException("The minimum value cannot be greater than the max value");
+        }
+
+        setMin(min);
+        setMax(max);
+        setValue(NumberUtils.clamp(initialValue, min, max));
+
+        initialize();
+    }
+
+    //================================================================================
+    // Methods
+    //================================================================================
+    private void initialize() {
+        getStyleClass().add(STYLE_CLASS);
+
+        defaultThumbSupplier();
+        defaultPopupSupplier();
+    }
+
+    protected void defaultThumbSupplier() {
+        setThumbSupplier(() -> {
+            MFXFontIcon thumb = new MFXFontIcon("mfx-circle", 12);
+            MFXFontIcon thumbRadius = new MFXFontIcon("mfx-circle", 30);
+            thumb.setMouseTransparent(true);
+            thumb.getStyleClass().setAll("thumb");
+            thumbRadius.getStyleClass().setAll("thumb-radius");
+
+            thumbRadius.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+                Node track = lookup(".track");
+                if (track != null) {
+                    Event.fireEvent(track, event);
+                }
+            });
+
+            StackPane stackPane = new StackPane();
+            stackPane.getStyleClass().add("thumb-container");
+            stackPane.setMinSize(USE_PREF_SIZE, USE_PREF_SIZE);
+            stackPane.setPrefSize(NodeUtils.getNodeWidth(thumb), NodeUtils.getNodeHeight(thumb));
+            stackPane.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
+            stackPane.getChildren().setAll(thumbRadius, thumb);
+
+            MFXCircleRippleGenerator rippleGenerator = new MFXCircleRippleGenerator(stackPane);
+            rippleGenerator.setAnimateBackground(false);
+            rippleGenerator.setAnimationSpeed(1.5);
+            rippleGenerator.setClipSupplier(() -> null);
+            rippleGenerator.setMouseTransparent(true);
+            rippleGenerator.setRippleRadius(6);
+            rippleGenerator.setRipplePositionFunction(mouseEvent -> new RipplePosition(stackPane.getWidth() / 2, stackPane.getHeight() / 2));
+            stackPane.addEventFilter(MouseEvent.MOUSE_PRESSED, rippleGenerator::generateRipple);
+            stackPane.getChildren().add(rippleGenerator);
+
+            return stackPane;
+        });
+    }
+
+    protected void defaultPopupSupplier() {
+        setPopupSupplier(() -> {
+            Label text = new Label();
+            text.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
+            text.setAlignment(Pos.CENTER);
+            text.setId("popupText");
+            text.textProperty().bind(Bindings.createStringBinding(
+                    () -> NumberUtils.formatToString(getValue(), getDecimalPrecision()),
+                    value
+            ));
+
+            MFXFontIcon caret = new MFXFontIcon("mfx-caret-down", 22);
+            caret.setId("popupCaret");
+            caret.setBoundsType(TextBoundsType.VISUAL);
+            caret.setManaged(false);
+
+            StackPane stackPane = new StackPane(text);
+            stackPane.setId("popupContent");
+            VBox.setVgrow(stackPane, Priority.ALWAYS);
+
+            VBox container = new VBox(stackPane, caret) {
+                @Override
+                protected void layoutChildren() {
+                    super.layoutChildren();
+
+                    Orientation orientation = getOrientation();
+                    double x = orientation == Orientation.HORIZONTAL ? (getWidth() / 2) - (caret.prefWidth(-1) / 2) : getHeight();
+                    double y = orientation == Orientation.HORIZONTAL ? getHeight() : -(caret.prefHeight(-1) / 2) + (getWidth() / 2);
+                    caret.relocate(snapPositionX(x), snapPositionY(y));
+                    caret.setRotate(orientation == Orientation.HORIZONTAL ? 0 : -90);
+                }
+            };
+            container.setAlignment(Pos.TOP_CENTER);
+            container.setMinSize(45, 40);
+            container.getStylesheets().add(STYLESHEET);
+
+            container.rotateProperty().bind(Bindings.createDoubleBinding(
+                    () -> {
+                        Orientation orientation = getOrientation();
+                        return orientation == Orientation.HORIZONTAL ? 0.0 : 90.0;
+                    }, orientationProperty())
+            );
+            return container;
+        });
+    }
+
+    public double getMin() {
+        return min.get();
+    }
+
+    public DoubleProperty minProperty() {
+        return min;
+    }
+
+    public void setMin(double min) {
+        this.min.set(min);
+    }
+
+    public double getMax() {
+        return max.get();
+    }
+
+    public DoubleProperty maxProperty() {
+        return max;
+    }
+
+    public void setMax(double max) {
+        this.max.set(max);
+    }
+
+    public double getValue() {
+        return value.get();
+    }
+
+    public DoubleProperty valueProperty() {
+        return value;
+    }
+
+    public void setValue(double value) {
+        this.value.set(value);
+    }
+
+    public Supplier<Node> getThumbSupplier() {
+        return thumbSupplier.get();
+    }
+
+    public ObjectProperty<Supplier<Node>> thumbSupplierProperty() {
+        return thumbSupplier;
+    }
+
+    public void setThumbSupplier(Supplier<Node> thumbSupplier) {
+        this.thumbSupplier.set(thumbSupplier);
+    }
+
+    public Supplier<Region> getPopupSupplier() {
+        return popupSupplier.get();
+    }
+
+    public ObjectProperty<Supplier<Region>> popupSupplierProperty() {
+        return popupSupplier;
+    }
+
+    public void setPopupSupplier(Supplier<Region> popupSupplier) {
+        this.popupSupplier.set(popupSupplier);
+    }
+
+    public int getDecimalPrecision() {
+        return decimalPrecision.get();
+    }
+
+    public IntegerProperty decimalPrecisionProperty() {
+        return decimalPrecision;
+    }
+
+    public void setDecimalPrecision(int decimalPrecision) {
+        this.decimalPrecision.set(decimalPrecision);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private final StyleableObjectProperty<SliderMode> sliderMode = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.SLIDER_MODE,
+            this,
+            "sliderMode",
+            SliderMode.DEFAULT
+    );
+
+    private final StyleableDoubleProperty unitIncrement = new SimpleStyleableDoubleProperty(
+            StyleableProperties.UNIT_INCREMENT,
+            this,
+            "unitIncrement",
+            10.0
+    );
+
+    private final StyleableDoubleProperty alternativeUnitIncrement = new SimpleStyleableDoubleProperty(
+            StyleableProperties.ALTERNATIVE_UNIT_INCREMENT,
+            this,
+            "alternativeUnitIncrement",
+            5.0
+    );
+
+
+    private final StyleableDoubleProperty tickUnit = new SimpleStyleableDoubleProperty(
+            StyleableProperties.TICK_UNIT,
+            this,
+            "tickUnit",
+            25.0
+    );
+
+    private final StyleableBooleanProperty showMajorTicks = new SimpleStyleableBooleanProperty(
+            StyleableProperties.SHOW_MAJOR_TICKS,
+            this,
+            "showMajorTicks",
+            false
+    );
+
+    private final StyleableBooleanProperty showMinorTicks = new SimpleStyleableBooleanProperty(
+            StyleableProperties.SHOW_MINOR_TICKS,
+            this,
+            "showMinorTicks",
+            false
+    );
+
+    private final StyleableBooleanProperty showTicksAtEdges = new SimpleStyleableBooleanProperty(
+            StyleableProperties.SHOW_TICKS_AT_EDGE,
+            this,
+            "showTicksAtEdge",
+            true
+    );
+
+    private final StyleableIntegerProperty minorTicksCount = new SimpleStyleableIntegerProperty(
+            StyleableProperties.MINOR_TICKS_COUNT,
+            this,
+            "minorTicksCount",
+            5
+    );
+
+    private final StyleableBooleanProperty animateOnPress = new SimpleStyleableBooleanProperty(
+            StyleableProperties.ANIMATE_ON_PRESS,
+            this,
+            "animateOnPress",
+            true
+    );
+
+    private final StyleableBooleanProperty bidirectional = new SimpleStyleableBooleanProperty(
+            StyleableProperties.BIDIRECTIONAL,
+            this,
+            "bidirectional",
+            true
+    );
+
+    private final StyleableObjectProperty<Orientation> orientation = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.ORIENTATION,
+            this,
+            "orientation",
+            Orientation.HORIZONTAL
+    );
+
+    private final StyleableObjectProperty<SliderPopupSide> popupSide = new SimpleStyleableObjectProperty<>(
+            StyleableProperties.POPUP_SIDE,
+            this,
+            "popupSide",
+            SliderPopupSide.DEFAULT
+    );
+
+    public SliderMode getSliderMode() {
+        return sliderMode.get();
+    }
+
+    public StyleableObjectProperty<SliderMode> sliderModeProperty() {
+        return sliderMode;
+    }
+
+    public void setSliderMode(SliderMode sliderMode) {
+        this.sliderMode.set(sliderMode);
+    }
+
+    public double getUnitIncrement() {
+        return unitIncrement.get();
+    }
+
+    public StyleableDoubleProperty unitIncrementProperty() {
+        return unitIncrement;
+    }
+
+    public void setUnitIncrement(double unitIncrement) {
+        this.unitIncrement.set(unitIncrement);
+    }
+
+    public double getAlternativeUnitIncrement() {
+        return alternativeUnitIncrement.get();
+    }
+
+    public StyleableDoubleProperty alternativeUnitIncrementProperty() {
+        return alternativeUnitIncrement;
+    }
+
+    public void setAlternativeUnitIncrement(double alternativeUnitIncrement) {
+        this.alternativeUnitIncrement.set(alternativeUnitIncrement);
+    }
+
+    public double getTickUnit() {
+        return tickUnit.get();
+    }
+
+    public StyleableDoubleProperty tickUnitProperty() {
+        return tickUnit;
+    }
+
+    public void setTickUnit(double tickUnit) {
+        this.tickUnit.set(tickUnit);
+    }
+
+    public boolean isShowMajorTicks() {
+        return showMajorTicks.get();
+    }
+
+    public StyleableBooleanProperty showMajorTicksProperty() {
+        return showMajorTicks;
+    }
+
+    public void setShowMajorTicks(boolean showMajorTicks) {
+        this.showMajorTicks.set(showMajorTicks);
+    }
+
+    public boolean isShowMinorTicks() {
+        return showMinorTicks.get();
+    }
+
+    public StyleableBooleanProperty showMinorTicksProperty() {
+        return showMinorTicks;
+    }
+
+    public void setShowMinorTicks(boolean showMinorTicks) {
+        this.showMinorTicks.set(showMinorTicks);
+    }
+
+    public boolean isShowTicksAtEdges() {
+        return showTicksAtEdges.get();
+    }
+
+    public StyleableBooleanProperty showTicksAtEdgesProperty() {
+        return showTicksAtEdges;
+    }
+
+    public void setShowTicksAtEdges(boolean showTicksAtEdges) {
+        this.showTicksAtEdges.set(showTicksAtEdges);
+    }
+
+    public int getMinorTicksCount() {
+        return minorTicksCount.get();
+    }
+
+    public StyleableIntegerProperty minorTicksCountProperty() {
+        return minorTicksCount;
+    }
+
+    public void setMinorTicksCount(int minorTicksCount) {
+        this.minorTicksCount.set(minorTicksCount);
+    }
+
+    public boolean isAnimateOnPress() {
+        return animateOnPress.get();
+    }
+
+    public StyleableBooleanProperty animateOnPressProperty() {
+        return animateOnPress;
+    }
+
+    public void setAnimateOnPress(boolean animateOnPress) {
+        this.animateOnPress.set(animateOnPress);
+    }
+
+    public boolean isBidirectional() {
+        return bidirectional.get();
+    }
+
+    public StyleableBooleanProperty bidirectionalProperty() {
+        return bidirectional;
+    }
+
+    public void setBidirectional(boolean bidirectional) {
+        this.bidirectional.set(bidirectional);
+    }
+
+    public Orientation getOrientation() {
+        return orientation.get();
+    }
+
+    public StyleableObjectProperty<Orientation> orientationProperty() {
+        return orientation;
+    }
+
+    public void setOrientation(Orientation orientation) {
+        this.orientation.set(orientation);
+    }
+
+    public SliderPopupSide getPopupSide() {
+        return popupSide.get();
+    }
+
+    public StyleableObjectProperty<SliderPopupSide> popupSideProperty() {
+        return popupSide;
+    }
+
+    public void setPopupSide(SliderPopupSide popupSide) {
+        this.popupSide.set(popupSide);
+    }
+
+    //================================================================================
+    // Styleable Properties
+    //================================================================================
+    private static class StyleableProperties {
+        private static final List<CssMetaData<? extends Styleable, ?>> cssMetaDataList;
+
+        private static final CssMetaData<MFXSlider, SliderMode> SLIDER_MODE =
+                FACTORY.createEnumCssMetaData(
+                        SliderMode.class,
+                        "-mfx-slider-mode",
+                        MFXSlider::sliderModeProperty,
+                        SliderMode.DEFAULT
+                );
+
+        private static final CssMetaData<MFXSlider, Number> UNIT_INCREMENT =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-unit-increment",
+                        MFXSlider::unitIncrementProperty,
+                        10.0
+                );
+
+        private static final CssMetaData<MFXSlider, Number> ALTERNATIVE_UNIT_INCREMENT =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-alternative-unit-increment",
+                        MFXSlider::alternativeUnitIncrementProperty,
+                        5.0
+                );
+
+        private static final CssMetaData<MFXSlider, Number> TICK_UNIT =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-tick-unit",
+                        MFXSlider::tickUnitProperty,
+                        25.0
+                );
+
+
+        private static final CssMetaData<MFXSlider, Boolean> SHOW_MAJOR_TICKS =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-show-major-ticks",
+                        MFXSlider::showMajorTicksProperty,
+                        false
+                );
+
+        private static final CssMetaData<MFXSlider, Boolean> SHOW_MINOR_TICKS =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-show-minor-ticks",
+                        MFXSlider::showMinorTicksProperty,
+                        false
+                );
+
+        private static final CssMetaData<MFXSlider, Boolean> SHOW_TICKS_AT_EDGE =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-show-ticks-at-edge",
+                        MFXSlider::showTicksAtEdgesProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXSlider, Number> MINOR_TICKS_COUNT =
+                FACTORY.createSizeCssMetaData(
+                        "-mfx-minor-ticks-count",
+                        MFXSlider::minorTicksCountProperty,
+                        5
+                );
+
+        private static final CssMetaData<MFXSlider, Boolean> ANIMATE_ON_PRESS =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-animate-on-press",
+                        MFXSlider::animateOnPressProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXSlider, Boolean> BIDIRECTIONAL =
+                FACTORY.createBooleanCssMetaData(
+                        "-mfx-bidirectional",
+                        MFXSlider::bidirectionalProperty,
+                        true
+                );
+
+        private static final CssMetaData<MFXSlider, Orientation> ORIENTATION =
+                FACTORY.createEnumCssMetaData(
+                        Orientation.class,
+                        "-mfx-orientation",
+                        MFXSlider::orientationProperty,
+                        Orientation.HORIZONTAL
+                );
+
+        private static final CssMetaData<MFXSlider, SliderPopupSide> POPUP_SIDE =
+                FACTORY.createEnumCssMetaData(
+                        SliderPopupSide.class,
+                        "-mfx-popup-side",
+                        MFXSlider::popupSideProperty,
+                        SliderPopupSide.DEFAULT
+                );
+
+        static {
+            cssMetaDataList = List.of(
+                    SLIDER_MODE,UNIT_INCREMENT, ALTERNATIVE_UNIT_INCREMENT,
+                    TICK_UNIT, SHOW_MAJOR_TICKS, SHOW_MINOR_TICKS, SHOW_TICKS_AT_EDGE, MINOR_TICKS_COUNT,
+                    ANIMATE_ON_PRESS, BIDIRECTIONAL, ORIENTATION, POPUP_SIDE
+            );
+        }
+    }
+
+    public static List<CssMetaData<? extends Styleable, ?>> getControlCssMetaDataList() {
+        return StyleableProperties.cssMetaDataList;
+    }
+
+    @Override
+    protected Skin<?> createDefaultSkin() {
+        return new MFXSliderSkin(this);
+    }
+
+    @Override
+    protected List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
+        return MFXSlider.getControlCssMetaDataList();
+    }
+
+    @Override
+    public String getUserAgentStylesheet() {
+        return STYLESHEET;
+    }
+}

+ 14 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/SliderEnum.java

@@ -0,0 +1,14 @@
+package io.github.palexdev.materialfx.controls.enums;
+
+public class SliderEnum {
+
+    private SliderEnum() {}
+
+    public enum SliderMode {
+        DEFAULT, SNAP_TO_TICKS
+    }
+
+    public enum SliderPopupSide {
+        DEFAULT, OTHER_SIDE
+    }
+}

+ 3 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/controls/enums/Styles.java

@@ -25,6 +25,9 @@ package io.github.palexdev.materialfx.controls.enums;
  * These emulators basically are just helpers that store the path to the right css file.
  */
 public class Styles {
+
+    private Styles() {}
+
     public enum ComboBoxStyles {
         STYLE1("css/MFXComboBoxStyle1.css"),
         STYLE2("css/MFXComboBoxStyle2.css"),

+ 650 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXSliderSkin.java

@@ -0,0 +1,650 @@
+package io.github.palexdev.materialfx.skins;
+
+import io.github.palexdev.materialfx.beans.NumberRange;
+import io.github.palexdev.materialfx.controls.MFXSlider;
+import io.github.palexdev.materialfx.controls.enums.SliderEnum.SliderMode;
+import io.github.palexdev.materialfx.controls.factories.MFXAnimationFactory;
+import io.github.palexdev.materialfx.font.MFXFontIcon;
+import io.github.palexdev.materialfx.utils.AnimationUtils;
+import io.github.palexdev.materialfx.utils.NodeUtils;
+import io.github.palexdev.materialfx.utils.NumberUtils;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.PauseTransition;
+import javafx.beans.binding.Bindings;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.event.EventHandler;
+import javafx.geometry.Orientation;
+import javafx.geometry.Point2D;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.chart.Axis.TickMark;
+import javafx.scene.chart.NumberAxis;
+import javafx.scene.control.SkinBase;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.shape.StrokeLineJoin;
+import javafx.scene.shape.StrokeType;
+import javafx.scene.transform.Rotate;
+import javafx.util.Duration;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class MFXSliderSkin extends SkinBase<MFXSlider> {
+    private final Rectangle track;
+    private final Rectangle bar;
+    private final Group group;
+    private final Group ticksGroup;
+    private final NumberAxis ticksAxis;
+    private Node thumb;
+    private Region valuePopup;
+
+    private final LayoutData layoutData = new LayoutData();
+
+    private double preDragThumbPos;
+    private Point2D dragStart;
+    private EventHandler<MouseEvent> thumbDragHandler;
+    private EventHandler<MouseEvent> thumbPressHandler;
+    private EventHandler<MouseEvent> trackPressedHandler;
+
+    private boolean mousePressed = false;
+    private boolean trackPressed = false;
+    private boolean keyPressed = false;
+    private boolean keyWasPressed = false;
+    private PauseTransition releaseTimer = new PauseTransition();
+
+    private boolean isSnapping = false;
+    private boolean wasSnapping = false;
+
+    public MFXSliderSkin(MFXSlider slider) {
+        super(slider);
+
+        track = buildRectangle("track");
+        track.heightProperty().bind(slider.heightProperty());
+        track.widthProperty().bind(slider.widthProperty());
+        track.setFill(Color.rgb(82, 0, 237, 0.3));
+        track.setStroke(Color.GOLD);
+
+        bar = buildRectangle("bar");
+        bar.heightProperty().bind(slider.heightProperty());
+        bar.setFill(Color.GREEN);
+        bar.setMouseTransparent(true);
+
+        thumb = slider.getThumbSupplier().get();
+        valuePopup = slider.getPopupSupplier().get();
+        valuePopup.setVisible(false);
+        valuePopup.setOpacity(0.0);
+
+        ticksAxis = new NumberAxis(slider.getMin(), slider.getMax(), slider.getTickUnit());
+        ticksAxis.setMinorTickCount(slider.getMinorTicksCount());
+        ticksAxis.setManaged(false);
+        ticksAxis.setMouseTransparent(true);
+        ticksAxis.setTickMarkVisible(false);
+        ticksAxis.setTickLabelsVisible(false);
+
+        Rectangle clip = new Rectangle();
+        clip.heightProperty().bind(slider.heightProperty());
+        clip.widthProperty().bind(slider.widthProperty());
+        clip.arcHeightProperty().bind(track.arcHeightProperty());
+        clip.arcWidthProperty().bind(track.arcWidthProperty());
+
+        ticksGroup = new Group(ticksAxis);
+        ticksGroup.setClip(clip);
+        ticksGroup.setManaged(false);
+        ticksGroup.setMouseTransparent(true);
+
+        group = new Group(track, ticksGroup, bar, thumb, valuePopup);
+        group.setManaged(false);
+        group.getStylesheets().add(slider.getUserAgentStylesheet());
+        getChildren().setAll(group);
+
+        releaseTimer.setDuration(Duration.millis(800));
+        releaseTimer.setOnFinished(event -> hidePopup());
+
+        thumbPressHandler = event -> {
+            dragStart = thumb.localToParent(event.getX(), event.getY());
+            preDragThumbPos = (slider.getValue() - slider.getMin()) / (slider.getMax() - slider.getMin());
+        };
+        thumbDragHandler = this::handleDrag;
+        trackPressedHandler = this::trackPressed;
+
+        if (slider.getOrientation() == Orientation.VERTICAL) {
+            slider.setRotate(-90);
+        } else {
+            slider.setRotate(0);
+        }
+
+        setBehavior();
+    }
+
+    /**
+     * Calls {@link #sliderHandlers()}, {@link #sliderListeners()}, {@link #skinBehavior()}.
+     */
+    protected void setBehavior() {
+        sliderHandlers();
+        sliderListeners();
+        skinBehavior();
+    }
+
+    private void sliderHandlers() {
+        MFXSlider slider = getSkinnable();
+
+        /* FOCUS */
+        slider.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> slider.requestFocus());
+
+        /* POPUP HANDLING */
+        slider.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
+            mousePressed = true;
+            Node intersectedNode = event.getPickResult().getIntersectedNode();
+            if (intersectedNode == track || NodeUtils.inHierarchy(intersectedNode, thumb)) {
+                showPopup();
+            }
+        });
+        slider.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
+            mousePressed = false;
+            releaseTimer.playFromStart();
+        });
+
+        /* KEYBOARD HANDLING */
+        slider.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
+            double val = (event.isShiftDown() || event.isControlDown()) ? slider.getAlternativeUnitIncrement() : slider.getUnitIncrement();
+
+            if (isIncreaseKey(event)) {
+                keyPressed = true;
+                keyWasPressed = true;
+                slider.setValue(
+                        NumberUtils.clamp(slider.getValue() + val, slider.getMin(), slider.getMax())
+                );
+            } else if (isDecreaseKey(event)) {
+                keyPressed = true;
+                keyWasPressed = true;
+                slider.setValue(
+                        NumberUtils.clamp(slider.getValue() - val, slider.getMin(), slider.getMax())
+                );
+            }
+        });
+    }
+
+    private void sliderListeners() {
+        MFXSlider slider = getSkinnable();
+
+        /* VALUE AND BOUNDS HANDLING */
+        slider.valueProperty().addListener((observable, oldValue, newValue) -> {
+            if (!isSnapping) {
+                updateLayout();
+            }
+        });
+        slider.minProperty().addListener((observable, oldValue, newValue) -> {
+            slider.setValue(0);
+            ticksAxis.setLowerBound(newValue.doubleValue());
+            slider.requestLayout();
+        });
+        slider.maxProperty().addListener((observable, oldValue, newValue) -> {
+            slider.setValue(0);
+            ticksAxis.setUpperBound(newValue.doubleValue());
+            slider.requestLayout();
+        });
+
+        /* NumberAxis HANDLING */
+        slider.minorTicksCountProperty().addListener((observable, oldValue, newValue) -> {
+            ticksAxis.setMinorTickCount(newValue.intValue());
+            ticksAxis.requestAxisLayout();
+            slider.requestLayout();
+        });
+        slider.tickUnitProperty().addListener((observable, oldValue, newValue) -> {
+            ticksAxis.setTickUnit(newValue.doubleValue());
+            ticksAxis.requestAxisLayout();
+            slider.requestLayout();
+        });
+        slider.showTicksAtEdgesProperty().addListener((observable, oldValue, newValue) -> slider.requestLayout());
+
+        /* SUPPLIERS HANDLING */
+        slider.popupSupplierProperty().addListener((observable, oldValue, newValue) -> {
+            handleValuePopupChange();
+            slider.requestLayout();
+        });
+        slider.thumbSupplierProperty().addListener((observable, oldValue, newValue) -> {
+            handleThumbChange();
+            slider.requestLayout();
+        });
+
+        /* FOCUS WORKAROUND HANDLING */
+        slider.focusedProperty().addListener((observable, oldValue, newValue) -> {
+            if (!newValue && keyPressed) {
+                AnimationUtils.PauseBuilder.build()
+                        .setDuration(Duration.millis(100))
+                        .runWhile(slider.isFocused(), slider::requestFocus, () -> keyPressed = false);
+            }
+        });
+
+        /* LAYOUT HANDLING */
+        slider.bidirectionalProperty().addListener((observable, oldValue, newValue) -> slider.requestLayout());
+        slider.orientationProperty().addListener((observable, oldValue, newValue) -> {
+            if (newValue == Orientation.VERTICAL) {
+                slider.setRotate(-90);
+            } else {
+                slider.setRotate(0);
+            }
+        });
+    }
+
+    private void skinBehavior() {
+        MFXSlider slider = getSkinnable();
+
+        /* THUMB AND TRACK HANDLING */
+        thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, thumbPressHandler);
+        thumb.addEventHandler(MouseEvent.MOUSE_DRAGGED, thumbDragHandler);
+        track.addEventHandler(MouseEvent.MOUSE_PRESSED, trackPressedHandler);
+
+        /* POPUP HANDLING */
+        valuePopup.layoutXProperty().bind(Bindings.createDoubleBinding(
+                () -> {
+                    double hx = thumb.getLayoutX() - (valuePopup.getWidth() / 2) + layoutData.halfThumbWidth();
+                    if (slider.getOrientation() == Orientation.HORIZONTAL) {
+                       return hx;
+                    }
+                    Rotate rotate = new Rotate(-90, hx, 0);
+                    return rotate.getPivotX() + 3;
+                },
+                thumb.layoutXProperty(), valuePopup.widthProperty(), slider.thumbSupplierProperty(), slider.orientationProperty()
+        ));
+        valuePopup.layoutYProperty().bind(Bindings.createDoubleBinding(
+                () -> {
+                    double hy = -(valuePopup.getHeight() + (layoutData.halfThumbHeight() * 2));
+                    if (slider.getOrientation() == Orientation.HORIZONTAL) {
+                        return hy;
+                    }
+                    Rotate rotate = new Rotate(-90, 0, hy);
+                    return rotate.getPivotY();
+                },
+                slider.heightProperty(), valuePopup.heightProperty(), slider.thumbSupplierProperty(), slider.orientationProperty()
+        ));
+
+        /* NumberAxis LAYOUT HANDLING */
+        ticksAxis.visibleProperty().bind(slider.showMinorTicksProperty());
+        ticksAxis.needsLayoutProperty().addListener((observable, oldValue, newValue) -> layoutData.updateTicksData());
+    }
+
+    private void updateLayout() {
+        MFXSlider slider = getSkinnable();
+
+        if (slider.getSliderMode() == SliderMode.SNAP_TO_TICKS && !keyWasPressed) {
+            isSnapping = true;
+            wasSnapping = true;
+            double closest = layoutData.findNearestTick();
+            slider.setValue(closest);
+            isSnapping = false;
+        }
+
+        layoutData.update(false);
+
+        if (!mousePressed) {
+            showPopup();
+            releaseTimer.playFromStart();
+        }
+
+        if ((trackPressed || wasSnapping) && slider.isAnimateOnPress()) {
+            keyWasPressed = false;
+            wasSnapping = false;
+            AnimationUtils.ParallelBuilder.build()
+                    .add(
+                            new KeyFrame(Duration.millis(200), new KeyValue(bar.layoutXProperty(), layoutData.barX, MFXAnimationFactory.getInterpolatorV1()))
+                    )
+                    .add(
+                            new KeyFrame(Duration.millis(200), new KeyValue(thumb.layoutXProperty(), layoutData.thumbX, MFXAnimationFactory.getInterpolatorV1())),
+                            new KeyFrame(Duration.millis(200), new KeyValue(bar.widthProperty(), Math.abs(layoutData.barW), MFXAnimationFactory.getInterpolatorV1()))
+                    )
+                    .getAnimation()
+                    .play();
+        } else {
+            thumb.setLayoutX(layoutData.thumbX);
+            bar.setLayoutX(layoutData.barX);
+            bar.setWidth(Math.abs(layoutData.barW));
+        }
+    }
+
+    private void handleDrag(MouseEvent event) {
+        MFXSlider slider = getSkinnable();
+        trackPressed = false;
+
+        Point2D curr = thumb.localToParent(event.getX(), event.getY());
+        double dragPos = curr.getX() - dragStart.getX();
+        double pos = preDragThumbPos + dragPos / slider.getWidth();
+        double val = NumberUtils.clamp((pos * (slider.getMax() - slider.getMin())) + slider.getMin(), slider.getMin(), slider.getMax());
+        slider.setValue(val);
+    }
+
+    private void trackPressed(MouseEvent event) {
+        MFXSlider slider = getSkinnable();
+        trackPressed = true;
+
+        double pos = event.getX() / slider.getWidth();
+        double val = NumberUtils.clamp((pos * (slider.getMax() - slider.getMin())) + slider.getMin(), slider.getMin(), slider.getMax());
+        slider.setValue(val);
+    }
+
+    private void handleValuePopupChange() {
+        MFXSlider slider = getSkinnable();
+
+        int index = -1;
+        if (valuePopup != null) {
+            index = group.getChildren().indexOf(valuePopup);
+            valuePopup.layoutXProperty().unbind();
+            valuePopup.layoutYProperty().unbind();
+            group.getChildren().remove(valuePopup);
+        }
+
+        Supplier<Region> popupSupplier = slider.getPopupSupplier();
+        valuePopup = popupSupplier != null ? popupSupplier.get() : null;
+
+        if (valuePopup != null) {
+            valuePopup.setVisible(false);
+            valuePopup.setOpacity(0.0);
+            valuePopup.layoutXProperty().bind(Bindings.createDoubleBinding(
+                    () -> thumb.getLayoutX() - (valuePopup.getWidth() / 2) + layoutData.halfThumbWidth(),
+                    thumb.layoutXProperty(), valuePopup.widthProperty()
+            ));
+            valuePopup.layoutYProperty().bind(Bindings.createDoubleBinding(
+                    () -> -(valuePopup.getHeight() + layoutData.halfThumbHeight()),
+                    slider.heightProperty(), valuePopup.heightProperty()
+            ));
+            group.getChildren().add(index >= 0 ? index : group.getChildren().size() - 1, valuePopup);
+        }
+    }
+
+    private void handleThumbChange() {
+        MFXSlider slider = getSkinnable();
+
+        int index = -1;
+        if (thumb != null) {
+            index = group.getChildren().indexOf(thumb);
+            thumb.removeEventHandler(MouseEvent.MOUSE_PRESSED, thumbPressHandler);
+            thumb.removeEventHandler(MouseEvent.MOUSE_DRAGGED, thumbDragHandler);
+            group.getChildren().remove(thumb);
+        }
+
+        Supplier<Node> thumbSupplier = slider.getThumbSupplier();
+        thumb = thumbSupplier != null ? thumbSupplier.get() : null;
+
+        if (thumb != null) {
+            thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, thumbPressHandler);
+            thumb.addEventHandler(MouseEvent.MOUSE_DRAGGED, thumbDragHandler);
+            group.getChildren().add(index >= 0 ? index : group.getChildren().size() - 1, thumb);
+
+            valuePopup.layoutXProperty().bind(Bindings.createDoubleBinding(
+                    () -> thumb.getLayoutX() - (valuePopup.getWidth() / 2) + layoutData.halfThumbWidth(),
+                    thumb.layoutXProperty(), valuePopup.widthProperty(), slider.thumbSupplierProperty()
+            ));
+            valuePopup.layoutYProperty().bind(Bindings.createDoubleBinding(
+                    () -> -(valuePopup.getHeight() + layoutData.halfThumbHeight()),
+                    slider.heightProperty(), valuePopup.heightProperty(), slider.thumbSupplierProperty()
+            ));
+        }
+    }
+
+    protected void showPopup() {
+        if (valuePopup == null) {
+            return;
+        }
+
+        releaseTimer.stop();
+        AnimationUtils.SequentialBuilder.build()
+                .add(AnimationUtils.PauseBuilder.build().setDuration(Duration.ONE).setOnFinished(event -> valuePopup.setVisible(true)).getAnimation())
+                .add(new KeyFrame(Duration.millis(200), new KeyValue(valuePopup.opacityProperty(), 1.0, Interpolator.EASE_IN)))
+                .getAnimation()
+                .play();
+    }
+
+    protected void hidePopup() {
+        if (valuePopup == null) {
+            return;
+        }
+
+        AnimationUtils.SequentialBuilder.build()
+                .add(new KeyFrame(Duration.millis(200), new KeyValue(valuePopup.opacityProperty(), 0.0, Interpolator.EASE_OUT)))
+                .setOnFinished(event -> valuePopup.setVisible(false))
+                .getAnimation()
+                .play();
+    }
+
+    /**
+     * Responsible for building the track and the bars for the progress bar.
+     */
+    protected Rectangle buildRectangle(String styleClass) {
+        Rectangle rectangle = new Rectangle();
+        rectangle.getStyleClass().setAll(styleClass);
+        rectangle.setStroke(Color.TRANSPARENT);
+        rectangle.setStrokeLineCap(StrokeLineCap.ROUND);
+        rectangle.setStrokeLineJoin(StrokeLineJoin.ROUND);
+        rectangle.setStrokeType(StrokeType.INSIDE);
+        rectangle.setStrokeWidth(0);
+        return rectangle;
+    }
+
+    protected Node buildTick() {
+        return new MFXFontIcon("mfx-circle", 4);
+    }
+
+    private boolean isIncreaseKey(KeyEvent event) {
+        MFXSlider slider = getSkinnable();
+
+        return (event.getCode() == KeyCode.UP && slider.getOrientation() == Orientation.VERTICAL) ||
+                (event.getCode() == KeyCode.RIGHT && slider.getOrientation() == Orientation.HORIZONTAL);
+    }
+
+    private boolean isDecreaseKey(KeyEvent event) {
+        MFXSlider slider = getSkinnable();
+
+        return (event.getCode() == KeyCode.DOWN && slider.getOrientation() == Orientation.VERTICAL) ||
+                (event.getCode() == KeyCode.LEFT && slider.getOrientation() == Orientation.HORIZONTAL);
+    }
+
+    @Override
+    protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset);
+    }
+
+    @Override
+    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return Math.max(6, bar.prefHeight(width)) + topInset + bottomInset;
+    }
+
+    @Override
+    protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return getSkinnable().prefWidth(height);
+    }
+
+    @Override
+    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+        return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+
+        thumb.removeEventHandler(MouseEvent.MOUSE_PRESSED, thumbPressHandler);
+        thumb.removeEventHandler(MouseEvent.MOUSE_DRAGGED, thumbDragHandler);
+        thumbPressHandler = null;
+        thumbDragHandler = null;
+
+        track.removeEventHandler(MouseEvent.MOUSE_PRESSED, trackPressedHandler);
+        trackPressedHandler = null;
+
+        releaseTimer = null;
+    }
+
+    @Override
+    protected void layoutChildren(double x, double y, double w, double h) {
+        super.layoutChildren(x, y, w, h);
+
+        layoutData.update(true);
+        thumb.relocate(layoutData.thumbX, layoutData.thumbY);
+        bar.relocate(layoutData.barX, 0);
+        bar.setWidth(Math.abs(layoutData.barW));
+
+        ticksAxis.resize(w, h);
+    }
+
+    protected class LayoutData {
+        private double zeroPos;
+        private double thumbX;
+        private double thumbY;
+        private double barW;
+        private double barX;
+
+        private final ObservableList<TickData> ticksData = FXCollections.observableArrayList();
+        private double ticksY;
+
+        public void update(boolean isFullUpdate) {
+            MFXSlider slider = getSkinnable();
+
+            if (isFullUpdate) {
+                double val;
+                if (slider.getMin() > 0) {
+                    val = slider.getMin();
+                } else if (slider.getMax() < 0) {
+                    val = slider.getMax();
+                } else {
+                    val = 0;
+                }
+
+                if (!slider.isBidirectional()) {
+                    val = slider.getMin();
+                }
+
+                zeroPos = NumberUtils.mapOneRangeToAnother(
+                        val,
+                        NumberRange.of(slider.getMin(), slider.getMax()),
+                        NumberRange.of(0.0, slider.getWidth()),
+                        slider.getDecimalPrecision()
+                ) + 1;
+            }
+
+            thumbX = snapPositionX(
+                    NumberUtils.mapOneRangeToAnother(
+                            slider.getValue(),
+                            NumberRange.of(slider.getMin(), slider.getMax()),
+                            NumberRange.of(0.0, slider.getWidth()),
+                            slider.getDecimalPrecision()
+                    ) - halfThumbWidth());
+            thumbY = snapPositionY(-halfThumbHeight() + (slider.getHeight() / 2));
+
+            if (!slider.isBidirectional()) {
+                barW = thumbX - zeroPos + (halfThumbWidth() * 3);
+                barX = zeroPos - halfThumbWidth();
+            } else {
+                barW = slider.getValue() < 0 ? thumbX - zeroPos - halfThumbWidth() : thumbX - zeroPos + (halfThumbWidth() * 3);
+                barX = slider.getValue() < 0 ? zeroPos + barW + halfThumbWidth() : zeroPos - halfThumbWidth();
+            }
+        }
+
+        public void updateTicksData() {
+            MFXSlider slider = getSkinnable();
+
+            List<Double> ticksX = ticksAxis.getTickMarks().stream()
+                    .map(TickMark::getPosition)
+                    .collect(Collectors.toList());
+
+            if (!ticksX.stream().allMatch(d -> d == 0)) {
+                ticksGroup.getChildren().removeAll(getTicks());
+                ticksData.clear();
+
+                ObservableList<TickMark<Number>> tickMarks = ticksAxis.getTickMarks();
+                for (int i = 0; i < tickMarks.size(); i++) {
+                    TickMark<Number> tickMark = ticksAxis.getTickMarks().get(i);
+                    TickData tickData = new TickData();
+                    tickData.tick = buildTick();
+                    tickData.tick.getStyleClass().setAll(NumberUtils.isEven(i) ? "tick-even" : "tick-odd");
+                    tickData.tickVal = (double) tickMark.getValue();
+                    tickData.x = snapPositionX(ticksX.get(i) - (tickData.halfTickWidth() / 1.5));
+                    ticksData.add(tickData);
+
+                    if (i == tickMarks.size() - 1) {
+                        tickData.x -= tickData.halfTickWidth();
+                    }
+                }
+                ticksY = snapPositionY(-ticksData.get(0).halfTickHeight() + (slider.getHeight() / 2));
+                positionTicks();
+            }
+        }
+
+        public void positionTicks() {
+            MFXSlider slider = getSkinnable();
+            if (!slider.isShowMajorTicks()) {
+                return;
+            }
+
+            for (int i = 0; i < ticksData.size(); i++) {
+                TickMark<Number> tickMark = ticksAxis.getTickMarks().get(i);
+                TickData tickData = ticksData.get(i);
+
+                if (!slider.isShowTicksAtEdges() &&
+                        ((double) tickMark.getValue() == slider.getMax() || (double) tickMark.getValue() == slider.getMin())
+                ) { continue; }
+
+                ticksGroup.getChildren().add(tickData.tick);
+                tickData.tick.relocate(tickData.x, ticksY);
+            }
+        }
+
+        public double findNearestTick() {
+            MFXSlider slider = getSkinnable();
+
+            double currVal = slider.getValue();
+            return NumberUtils.closestValueTo(currVal, ticksData.stream().map(TickData::getTickVal).collect(Collectors.toList()));
+        }
+
+        public List<Node> getTicks() {
+            return ticksData.stream().map(TickData::getTick).collect(Collectors.toList());
+        }
+
+        public double halfThumbWidth() {
+            return thumb.prefWidth(-1) / 2;
+        }
+
+        public double halfThumbHeight() {
+            return thumb.prefHeight(-1) / 2;
+        }
+    }
+
+    protected static class TickData {
+        private Node tick;
+        private double tickVal;
+        private double x;
+
+        public Node getTick() {
+            return tick;
+        }
+
+        public double getTickVal() {
+            return tickVal;
+        }
+
+        public double getX() {
+            return x;
+        }
+
+        public double halfTickHeight() {
+            return tick == null ? 0 : tick.prefHeight(-1) / 2;
+        }
+
+        public double halfTickWidth() {
+            return tick == null ? 0 : tick.prefWidth(-1) / 2;
+        }
+    }
+}

+ 2 - 2
materialfx/src/main/java/io/github/palexdev/materialfx/skins/MFXTreeItemSkin.java

@@ -109,7 +109,7 @@ public class MFXTreeItemSkin<T> extends SkinBase<MFXTreeItem<T>> {
         box.setMinHeight(Region.USE_PREF_SIZE);
         box.setMaxHeight(Region.USE_PREF_SIZE);
 
-        item.setInitialHeight(NodeUtils.getNodeHeight(box));
+        item.setInitialHeight(NodeUtils.getRegionHeight(box));
         getChildren().add(box);
         box.setPrefHeight(item.getInitialHeight());
 
@@ -154,7 +154,7 @@ public class MFXTreeItemSkin<T> extends SkinBase<MFXTreeItem<T>> {
                 item.fireEvent(new TreeItemEvent<>(TreeItemEvent.ADD_REMOVE_ITEM_EVENT, item, -value));
             }
             if (!tmpAdded.isEmpty() && (item.isExpanded() || item.isStartExpanded())) {
-                double value = tmpAdded.stream().mapToDouble(NodeUtils::getNodeHeight).sum();
+                double value = tmpAdded.stream().mapToDouble(NodeUtils::getRegionHeight).sum();
                 box.getChildren().addAll(tmpAdded);
                 FXCollections.sort(box.getChildren(), Comparator.comparingInt(item.getItems()::indexOf));
                 item.fireEvent(new TreeItemEvent<>(TreeItemEvent.ADD_REMOVE_ITEM_EVENT, item, value));

+ 68 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/AnimationUtils.java

@@ -12,6 +12,9 @@ import javafx.scene.text.Text;
 import javafx.stage.Window;
 import javafx.util.Duration;
 
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
 /**
  * Utility class to easily build animations of any sort. Designed with fluent api.
  */
@@ -179,6 +182,45 @@ public class AnimationUtils {
             return this;
         }
 
+        /**
+         * Gets the animation from the supplier and adds it to the "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(Supplier<Animation> animationSupplier) {
+            addAnimation(animationSupplier.get());
+            return this;
+        }
+
+        /**
+         * Gets the animation from the supplier, sets the given onFinished action to it and then adds it to the
+         * "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(Supplier<Animation> animationSupplier, EventHandler<ActionEvent> onFinished) {
+            Animation animation = animationSupplier.get();
+            animation.setOnFinished(onFinished);
+            addAnimation(animation);
+            return this;
+        }
+
+        /**
+         * Builds a {@link Timeline} with the given keyframes and adds it to the "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(KeyFrame... keyFrames) {
+            addAnimation(new Timeline(keyFrames));
+            return this;
+        }
+
+        /**
+         * Builds a {@link Timeline} with the given keyframes, sets the given onFinished action to it and then adds it to the
+         * "main" animation by calling {@link #addAnimation(Animation)}.
+         */
+        public AbstractBuilder add(EventHandler<ActionEvent> onFinished, KeyFrame... keyFrames) {
+            Timeline timeline = new Timeline(keyFrames);
+            timeline.setOnFinished(onFinished);
+            addAnimation(timeline);
+            return this;
+        }
+
+
         /**
          * For each given node builds and adds an animation that disables the node
          * after the given duration of time.
@@ -482,5 +524,31 @@ public class AnimationUtils {
         public PauseTransition getAnimation() {
             return pauseTransition;
         }
+
+        public void runWhile(boolean condition, Runnable retryAction, Runnable onSuccessAction) {
+            setOnFinished(event -> {
+                if (!condition) {
+                    retryAction.run();
+                    getAnimation().playFromStart();
+                } else {
+                    onSuccessAction.run();
+                }
+            });
+            getAnimation().play();
+        }
+
+        public void runWhile(boolean condition, Runnable retryAction, Runnable onSuccessAction, int maxRetryCount) {
+            AtomicInteger retryCount = new AtomicInteger(0);
+            setOnFinished(event -> {
+                if (!condition && retryCount.get() < maxRetryCount) {
+                    retryCount.getAndIncrement();
+                    retryAction.run();
+                    getAnimation().playFromStart();
+                } else {
+                    onSuccessAction.run();
+                }
+            });
+            getAnimation().play();
+        }
     }
 }

+ 21 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/ExecutionUtils.java

@@ -1,6 +1,9 @@
 package io.github.palexdev.materialfx.utils;
 
 import javafx.application.Platform;
+import javafx.beans.binding.BooleanExpression;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
 
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
@@ -98,4 +101,22 @@ public class ExecutionUtils {
             return null;
         }
     }
+
+    public static void executeWhen(BooleanExpression condition, Runnable action, boolean isOneShot) {
+        if (condition.get()) {
+            action.run();
+        } else {
+            condition.addListener(new ChangeListener<>() {
+                @Override
+                public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
+                    if (newValue != null) {
+                        action.run();
+                        if (isOneShot) {
+                            condition.removeListener(this);
+                        }
+                    }
+                }
+            });
+        }
+    }
 }

+ 3 - 3
materialfx/src/main/java/io/github/palexdev/materialfx/utils/LabelUtils.java

@@ -69,7 +69,7 @@ public class LabelUtils {
     }
 
     /**
-     * Computes the min width of a text node so that all the text is visible. Uses {@link NodeUtils#getNodeWidth(Region)}.
+     * Computes the min width of a text node so that all the text is visible. Uses {@link NodeUtils#getRegionWidth(Region)}.
      * <p>
      * Uses {@link Label} as helper.
      *
@@ -81,7 +81,7 @@ public class LabelUtils {
         helper.setMaxWidth(Double.MAX_VALUE);
         helper.setFont(font);
 
-        return NodeUtils.getNodeWidth(helper);
+        return NodeUtils.getRegionWidth(helper);
     }
 
     /**
@@ -96,7 +96,7 @@ public class LabelUtils {
         Label helper = new Label(text);
         helper.setMaxWidth(Double.MAX_VALUE);
         helper.setFont(font);
-        return NodeUtils.getNodeHeight(helper);
+        return NodeUtils.getRegionHeight(helper);
     }
 
     /**

+ 44 - 6
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NodeUtils.java

@@ -179,32 +179,70 @@ public class NodeUtils {
         }
     }
 
+    /**
+     * Retrieves the region height if it isn't still laid out.
+     *
+     * @param region the Region of which to know the height
+     * @return the calculated height
+     */
+    public static double getRegionHeight(Region region) {
+        Group group = new Group(region);
+        Scene scene = new Scene(group);
+        group.applyCss();
+        group.layout();
+
+        group.getChildren().clear();
+        return region.getHeight();
+    }
+
     /**
      * Retrieves the region width if it isn't still laid out.
      *
      * @param region the Region of which to know the width
      * @return the calculated width
      */
-    public static double getNodeWidth(Region region) {
+    public static double getRegionWidth(Region region) {
         Group group = new Group(region);
         Scene scene = new Scene(group);
         group.applyCss();
         group.layout();
+
+        group.getChildren().clear();
         return region.getWidth();
     }
 
     /**
-     * Retrieves the region height if it isn't still laid out.
+     * Retrieves the node height if it isn't still laid out.
      *
-     * @param region the Region of which to know the height
+     * @param node the Node of which to know the height
      * @return the calculated height
      */
-    public static double getNodeHeight(Region region) {
-        Group group = new Group(region);
+    public static double getNodeHeight(Node node) {
+        Group group = new Group(node);
         Scene scene = new Scene(group);
         group.applyCss();
         group.layout();
-        return region.getHeight();
+
+        double height = node.prefHeight(-1);
+        group.getChildren().clear();
+        return height;
+    }
+
+    /**
+     * Retrieves the node width if it isn't still laid out.
+     *
+     * @param node the Node of which to know the width
+     * @return the calculated width
+     */
+    public static double getNodeWidth(Node node) {
+        Group group = new Group(node);
+        Scene scene = new Scene(group);
+        group.applyCss();
+        group.layout();
+
+        double width = node.prefWidth(-1);
+        group.getChildren().clear();
+        return width;
     }
 
     /**

+ 60 - 0
materialfx/src/main/java/io/github/palexdev/materialfx/utils/NumberUtils.java

@@ -0,0 +1,60 @@
+package io.github.palexdev.materialfx.utils;
+
+import io.github.palexdev.materialfx.beans.NumberRange;
+
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class NumberUtils {
+
+    private NumberUtils() {
+    }
+
+    public static double clamp(double val, double min, double max) {
+        return Math.max(min, Math.min(max, val));
+    }
+
+    public static double mapOneRangeToAnother(double value, NumberRange<Double> fromRange, NumberRange<Double> toRange, int decimalPrecision) {
+        double deltaA = fromRange.getMax() - fromRange.getMin();
+        double deltaB = toRange.getMax() - toRange.getMin();
+        double scale = deltaB / deltaA;
+        double negA = -1 * fromRange.getMin();
+        double offset = (negA * scale) + toRange.getMin();
+        double finalNumber = (value * scale) + offset;
+        int calcScale = (int) Math.pow(10, decimalPrecision);
+        return (double) Math.round(finalNumber * calcScale) / calcScale;
+    }
+
+    public static double closestValueTo(double val, List<Double> list) {
+        if (list.isEmpty()) {
+            return 0.0;
+        }
+
+        double res = list.get(0);
+        for (int i = 1; i < list.size(); i++) {
+            if (Math.abs(val - res) >
+                    Math.abs(val - list.get(i))) {
+                res = list.get(i);
+            }
+        }
+
+        return res;
+    }
+
+    public static double formatTo(double value, int decimalPrecision) {
+        int calcScale = (int) Math.pow(10, decimalPrecision);
+        return (double) Math.round(value * calcScale) / calcScale;
+    }
+
+    public static String formatToString(double value, int decimalPrecision) {
+        return String.format("%." + decimalPrecision + "f", value);
+    }
+
+    public static double getRandomDoubleBetween(double min, double max) {
+        return ThreadLocalRandom.current().nextDouble(min, max);
+    }
+
+    public static boolean isEven(int number) {
+        return (number % 2 == 0);
+    }
+}

+ 2 - 0
materialfx/src/main/java/module-info.java

@@ -9,6 +9,8 @@ module MaterialFX {
     exports io.github.palexdev.materialfx;
     exports io.github.palexdev.materialfx.beans;
     exports io.github.palexdev.materialfx.beans.binding;
+    exports io.github.palexdev.materialfx.beans.properties;
+    exports io.github.palexdev.materialfx.beans.properties.base;
     exports io.github.palexdev.materialfx.collections;
     exports io.github.palexdev.materialfx.controls;
     exports io.github.palexdev.materialfx.controls.base;

+ 81 - 0
materialfx/src/main/resources/io/github/palexdev/materialfx/css/MFXSlider.css

@@ -0,0 +1,81 @@
+@import "Fonts.css";
+
+/* COLORS */
+.mfx-slider {
+    -mfx-main-color: rgb(98, 0, 238);
+    -mfx-main-color-hover: rgba(98, 0, 238, 0.1);
+    -mfx-main-color-pressed: rgba(98, 0, 238, 0.3);
+    -mfx-disabled-color: rgb(158, 158, 158);
+}
+
+/* MAIN STYLING */
+.mfx-slider .track,
+.mfx-slider .bar {
+    -fx-arc-height: 6;
+    -fx-arc-width: 6;
+}
+
+.mfx-slider .track {
+    -fx-fill: derive(-mfx-main-color, 140%);
+}
+
+.mfx-slider .bar {
+    -fx-fill: -mfx-main-color;
+}
+
+.mfx-slider .thumb-container .thumb {
+    -mfx-color: -mfx-main-color;
+}
+
+.mfx-slider .thumb-container .thumb-radius {
+    -mfx-color: transparent;
+}
+
+.mfx-slider .thumb-container:hover .thumb-radius {
+    -mfx-color: -mfx-main-color-hover;
+}
+
+.mfx-slider .thumb-container:pressed .thumb-radius {
+    -mfx-color: -mfx-main-color-pressed;
+}
+
+/* TICKS STYLING */
+.mfx-slider .tick-even,
+.mfx-slider .tick-odd,
+.mfx-slider .axis-minor-tick-mark {
+    -mfx-color: derive(-mfx-main-color, 30%);
+    -fx-stroke: derive(-mfx-main-color, 130%);
+}
+
+/* POPUP */
+#popupContent {
+    -fx-background-color: #6E6E6E;
+    -fx-background-radius: 6;
+}
+
+#popupText {
+    -fx-font-family: "Open Sans SemiBold";
+    -fx-font-size: 13;
+    -fx-text-fill: white;
+}
+
+#popupCaret {
+    -mfx-color: #6E6E6E;
+}
+
+/* DISABLED */
+.mfx-slider:disabled .bar,
+.mfx-slider:disabled .tick-even,
+.mfx-slider:disabled .tick-odd,
+.mfx-slider:disabled .thumb-container .thumb {
+    -mfx-color: -mfx-disabled-color;
+    -fx-fill: -mfx-disabled-color;
+}
+
+.mfx-slider:disabled .track {
+    -fx-fill: derive(-mfx-disabled-color, 75%);
+}
+
+.mfx-slider:disabled .axis-minor-tick-mark {
+    -fx-stroke: derive(-mfx-disabled-color, 60%)
+}