Improve slider

- Add control skin to support changing progress color
- Implement control size support (small, medium, large)
This commit is contained in:
mkpaz 2022-09-30 13:33:53 +04:00
parent a295fd799c
commit ef930a7907
5 changed files with 286 additions and 75 deletions

@ -0,0 +1,52 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.geometry.Orientation;
import javafx.scene.control.Slider;
import javafx.scene.control.skin.SliderSkin;
import javafx.scene.layout.StackPane;
/** {@link Slider} skin that supports progress color. */
public class ProgressSliderSkin extends SliderSkin {
protected final StackPane thumb;
protected final StackPane track;
protected final StackPane progressTrack;
public ProgressSliderSkin(Slider slider) {
super(slider);
track = (StackPane) getSkinnable().lookup(".track");
thumb = (StackPane) getSkinnable().lookup(".thumb");
progressTrack = new StackPane();
progressTrack.getStyleClass().add("progress");
progressTrack.setMouseTransparent(true);
getSkinnable().getStyleClass().add("progress-slider");
getChildren().add(getChildren().indexOf(thumb), progressTrack);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
double progressX, progressY, progressWidth, progressHeight;
// intentionally ignore background radius in calculation,
// because slider looks better this way
if (getSkinnable().getOrientation() == Orientation.HORIZONTAL) {
progressX = track.getLayoutX();
progressY = track.getLayoutY();
progressWidth = thumb.getLayoutX() - snappedLeftInset();
progressHeight = track.getHeight();
} else {
progressX = track.getLayoutX();
progressY = thumb.getLayoutY();
progressWidth = track.getWidth();
progressHeight = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset();
}
progressTrack.resizeRelocate(progressX, progressY, progressWidth, progressHeight);
}
}

@ -1,6 +1,7 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.IntegerStringConverter;
@ -235,6 +236,7 @@ public class OverviewPage extends AbstractPage {
tickSlider.setMinorTickCount(5);
tickSlider.setSnapToTicks(true);
tickSlider.setPrefWidth(BUTTON_WIDTH * 2);
tickSlider.setSkin(new ProgressSliderSkin(tickSlider));
var container = new HBox(BLOCK_HGAP, slider, tickSlider);
return new SampleBlock("Sliders", container);

@ -1,16 +1,16 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Slider;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
public class SliderPage extends AbstractPage {
@ -26,39 +26,87 @@ public class SliderPage extends AbstractPage {
super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP,
horizontalSample(),
verticalSample(),
basicSample(),
smallSample(),
largeSample(),
disabledSample()
));
}
private SampleBlock horizontalSample() {
var slider = new Slider(1, 5, 3);
slider.setOrientation(HORIZONTAL);
private SampleBlock basicSample() {
var hSlider = new Slider(1, 5, 3);
var tickSlider = createTickSlider();
tickSlider.setMinWidth(SLIDER_SIZE);
tickSlider.setMaxWidth(SLIDER_SIZE);
var hTickSlider = createTickSlider();
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
return new SampleBlock("Horizontal", new VBox(SPACING, slider, tickSlider));
var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL);
var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
return new SampleBlock("Basic", createContent(hSlider, hTickSlider, vSlider, vTickSlider));
}
private Pane verticalSample() {
var slider = new Slider(1, 5, 3);
slider.setOrientation(VERTICAL);
private Pane smallSample() {
var hSlider = new Slider(1, 5, 3);
hSlider.getStyleClass().add(Styles.SMALL);
var tickSlider = createTickSlider();
tickSlider.setOrientation(VERTICAL);
tickSlider.setMinHeight(SLIDER_SIZE);
tickSlider.setMaxHeight(SLIDER_SIZE);
var hTickSlider = createTickSlider();
hTickSlider.getStyleClass().add(Styles.SMALL);
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
return new SampleBlock("Vertical", new HBox(SPACING, slider, tickSlider));
var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL);
vSlider.getStyleClass().add(Styles.SMALL);
var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL);
vTickSlider.getStyleClass().add(Styles.SMALL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
return new SampleBlock("Small", createContent(hSlider, hTickSlider, vSlider, vTickSlider));
}
private Pane largeSample() {
var hSlider = new Slider(1, 5, 3);
hSlider.getStyleClass().add(Styles.LARGE);
var hTickSlider = createTickSlider();
hTickSlider.getStyleClass().add(Styles.LARGE);
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL);
vSlider.getStyleClass().add(Styles.LARGE);
var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL);
vTickSlider.getStyleClass().add(Styles.LARGE);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
return new SampleBlock("Large", createContent(hSlider, hTickSlider, vSlider, vTickSlider));
}
private Pane disabledSample() {
var disabledSlider = createTickSlider();
disabledSlider.setDisable(true);
return new SampleBlock("Disabled", new HBox(disabledSlider));
var hSlider = new Slider(1, 5, 3);
hSlider.setDisable(true);
var hTickSlider = createTickSlider();
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
hTickSlider.setDisable(true);
var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL);
vSlider.setDisable(true);
var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
vTickSlider.setDisable(true);
return new SampleBlock("Disabled", createContent(hSlider, hTickSlider, vSlider, vTickSlider));
}
private Slider createTickSlider() {
@ -71,4 +119,24 @@ public class SliderPage extends AbstractPage {
slider.setSnapToTicks(true);
return slider;
}
private GridPane createContent(Slider h1, Slider h2, Slider v1, Slider v2) {
var grid = new GridPane();
grid.setVgap(SPACING);
grid.setHgap(SPACING);
h1.setPrefWidth(SLIDER_SIZE);
h2.setPrefWidth(SLIDER_SIZE);
v1.setPrefHeight(SLIDER_SIZE);
v2.setPrefHeight(SLIDER_SIZE);
grid.add(h1, 0, 0);
grid.add(h2, 0, 1);
grid.add(v1, 1, 0, 1, GridPane.REMAINING);
grid.add(v2, 2, 0, 1, GridPane.REMAINING);
return grid;
}
}

@ -2,6 +2,7 @@
package atlantafx.sampler.page.showcase.musicplayer;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.controls.Spacer;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
@ -115,6 +116,7 @@ final class PlayerPane extends VBox {
// == TIME CONTROLS ==
timeSlider = new Slider(0, 1, 0);
timeSlider.setSkin(new ProgressSliderSkin(timeSlider));
timeSlider.getStyleClass().add("time-slider");
timeSlider.setMinWidth(PANEL_MAX_WIDTH);
timeSlider.setMaxWidth(PANEL_MAX_WIDTH);
@ -144,6 +146,8 @@ final class PlayerPane extends VBox {
shuffleBtn.setOnAction(e -> model.shuffle());
volumeSlider = new Slider(0, 1, 0.75);
volumeSlider.setSkin(new ProgressSliderSkin(volumeSlider));
volumeSlider.getStyleClass().add(SMALL);
volumeSlider.setOrientation(VERTICAL);
var volumeBar = new VBox(5);
@ -169,22 +173,7 @@ final class PlayerPane extends VBox {
setAlignment(CENTER);
setSpacing(5);
setMinWidth(300);
getChildren().setAll(
new Spacer(VERTICAL),
new StackPane(coverImage),
new Spacer(10, VERTICAL),
trackTitle,
trackArtist,
trackAlbum,
new Spacer(20, VERTICAL),
mediaControls,
new Spacer(10, VERTICAL),
timeSlider,
timeMarkersBox,
new Spacer(10, VERTICAL),
extraControls,
new Spacer(VERTICAL)
);
getChildren().setAll(new Spacer(VERTICAL), new StackPane(coverImage), new Spacer(10, VERTICAL), trackTitle, trackArtist, trackAlbum, new Spacer(20, VERTICAL), mediaControls, new Spacer(10, VERTICAL), timeSlider, timeMarkersBox, new Spacer(10, VERTICAL), extraControls, new Spacer(VERTICAL));
}
private void init() {

@ -3,61 +3,77 @@
@use "../settings/config" as cfg;
@use "sass:math";
$color-thumb: if(cfg.$darkMode, -color-fg-default, -color-accent-emphasis) !default;
$color-thumb-border: if(cfg.$darkMode, -color-fg-default, -color-accent-emphasis) !default;
$color-track: -color-accent-emphasis !default;
$color-tick: -color-fg-muted !default;
$color-thumb: if(cfg.$darkMode, -color-fg-default, -color-accent-emphasis) !default;
$color-thumb-border: $color-thumb !default;
$color-track: -color-border-muted !default;
$color-track-progress: -color-accent-emphasis !default;
$color-tick: -color-fg-muted !default;
$thumb-size: 8px !default;
$thumb-border-width: 2px !default;
// this is padding ...
$thumb-size: (
"small": 8px,
"medium": 10px,
"large": 12px
) !default;
$track-size: $thumb-size !default; // visual track height (or width)
$track-margin: 6px !default; // increases clickable track area
// ... in combination with radius it can be converted to square or rectange
$thumb-radius: 10em !default;
$tick-major-size: 5px !default;
$tick-minor-size: 3px !default;
// visual track height (or width)
$track-size: (
"small": 2px,
"medium": 4px,
"large": 12px
) !default;
$_track-padding: math.div($track-size + $track-margin, 2);
$track-radius: cfg.$border-radius !default;
$tick-major-size: 5px !default;
$tick-minor-size: 3px !default;
.slider {
-color-slider-thumb: $color-thumb;
-color-slider-thumb-border: $color-thumb-border;
-color-slider-track: $color-track;
-color-slider-tick: $color-tick;
-color-slider-thumb: $color-thumb;
-color-slider-thumb-border: $color-thumb-border;
-color-slider-track: $color-track;
-color-slider-track-progress: $color-track-progress;
-color-slider-tick: $color-tick;
&.large {
-color-slider-thumb: if(cfg.$darkMode, $color-thumb, -color-fg-emphasis);
-color-slider-thumb-border: if(cfg.$darkMode, $color-thumb, -color-accent-emphasis);
}
>.thumb {
-fx-background-color: -color-slider-thumb-border, -color-slider-thumb;
-fx-background-insets: 0, 2px;
-fx-background-radius: 50;
-fx-padding: $thumb-size;
-fx-background-radius: $thumb-radius;
-fx-padding: map-get($thumb-size, "medium");
}
&.small {
>.thumb {
-fx-padding: map-get($thumb-size, "small");
}
}
&.large {
>.thumb {
-fx-padding: map-get($thumb-size, "large");
}
}
>.track {
// transparent background increases clickable track area without increasing visual track height,
// it's also used to center track with thumb
-fx-background-color: transparent, -color-slider-track;
-fx-background-radius: cfg.$border-radius;
-fx-background-radius: $track-radius;
}
// center thumb over track horizontally
&:horizontal {
>.track {
-fx-padding: $_track-padding 0 $_track-padding 0;
-fx-background-insets: 0, $track-margin 0 $track-margin 0;
}
}
// center thumb over track vertically
&:vertical {
>.track {
-fx-padding: 0 $_track-padding 0 $_track-padding;
-fx-background-insets: 0, 0 $track-margin 0 $track-margin;
}
>.progress {
-fx-background-color: transparent, -color-slider-track-progress;
}
// there's slightly noticable difference between axis length and track length,
// wontfix this via CSS, because it's probably JavaFX calc problem
// because SliderSkin ignores track radius in layoutChildren()
>.axis {
-fx-tick-label-fill: -color-slider-tick;
-fx-tick-length: $tick-major-size;
@ -72,4 +88,88 @@ $_track-padding: math.div($track-size + $track-margin, 2);
&:disabled {
-fx-opacity: cfg.$opacity-disabled;
}
/////////////////////////////////////////////////////////
// Horizontal //
/////////////////////////////////////////////////////////
// center thumb over track horizontally
&:horizontal {
>.track {
-fx-padding: map-get($thumb-size, "medium") 0 map-get($thumb-size, "medium") 0;
-fx-background-insets: 0, calc(map-get($thumb-size, "medium") - map-get($track-size, "medium")) 0
calc(map-get($thumb-size, "medium") - map-get($track-size, "medium")) 0;
}
>.progress {
-fx-background-radius: $track-radius;
-fx-background-insets: 0, calc(map-get($thumb-size, "medium") - map-get($track-size, "medium")) 0
calc(map-get($thumb-size, "medium") - map-get($track-size, "medium")) 0;
}
}
&.small:horizontal {
>.track {
-fx-padding: map-get($thumb-size, "small") 0 map-get($thumb-size, "small") 0;
-fx-background-insets: 0, calc(map-get($thumb-size, "small") - map-get($track-size, "small")) 0
calc(map-get($thumb-size, "small") - map-get($track-size, "small")) 0;
}
>.progress {
-fx-padding: map-get($thumb-size, "small") 0 map-get($thumb-size, "small") 0;
-fx-background-insets: 0, calc(map-get($thumb-size, "small") - map-get($track-size, "small")) 0
calc(map-get($thumb-size, "small") - map-get($track-size, "small")) 0;
}
}
&.large:horizontal {
>.track {
-fx-padding: map-get($track-size, "large") 0 map-get($track-size, "large") 0;
-fx-background-insets: 0;
}
>.progress {
-fx-padding: map-get($track-size, "large") 0 map-get($track-size, "large") 0;
-fx-background-insets: 0;
}
}
/////////////////////////////////////////////////////////
// Vertical //
/////////////////////////////////////////////////////////
// center thumb over track vertically
&:vertical {
>.track {
-fx-padding: 0 map-get($thumb-size, "medium") 0 map-get($thumb-size, "medium");
-fx-background-insets: 0, 0 calc(map-get($thumb-size, "medium") - map-get($track-size, "medium"))
0 calc(map-get($thumb-size, "medium") - map-get($track-size, "medium"));
}
>.progress {
-fx-background-radius: $thumb-radius $thumb-radius $track-radius $track-radius;
-fx-background-insets: 0, 0 calc(map-get($thumb-size, "medium") - map-get($track-size, "medium"))
0 calc(map-get($thumb-size, "medium") - map-get($track-size, "medium"));
}
}
&.small:vertical {
>.track {
-fx-padding: 0 map-get($thumb-size, "small") 0 map-get($thumb-size, "small");
-fx-background-insets: 0, 0 calc(map-get($thumb-size, "small") - map-get($track-size, "small"))
0 calc(map-get($thumb-size, "small") - map-get($track-size, "small"));
}
>.progress {
-fx-padding: map-get($thumb-size, "small") 0 map-get($thumb-size, "small") 0;
-fx-background-insets: 0, 0 calc(map-get($thumb-size, "small") - map-get($track-size, "small"))
0 calc(map-get($thumb-size, "small") - map-get($track-size, "small"));
}
}
&.large:vertical {
>.track {
-fx-padding: 0 map-get($track-size, "large") 0 map-get($track-size, "large");
-fx-background-insets: 0;
}
>.progress {
-fx-padding: 0 map-get($track-size, "large") 0 map-get($track-size, "large");
-fx-background-insets: 0;
}
}
}