diff --git a/base/src/main/java/atlantafx/base/controls/ProgressSliderSkin.java b/base/src/main/java/atlantafx/base/controls/ProgressSliderSkin.java new file mode 100644 index 0000000..3b47a9b --- /dev/null +++ b/base/src/main/java/atlantafx/base/controls/ProgressSliderSkin.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java index cd567b2..95e3ac4 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/OverviewPage.java @@ -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); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java index 3cb7931..fcd48d9 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/SliderPage.java @@ -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; + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/PlayerPane.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/PlayerPane.java index 95ef704..94b3e8d 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/PlayerPane.java +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/PlayerPane.java @@ -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() { @@ -284,4 +273,4 @@ final class PlayerPane extends VBox { Object tag = media.getMetadata().get(key); return type.isInstance(tag) ? type.cast(tag) : defaultValue; } -} \ No newline at end of file +} diff --git a/styles/src/components/_slider.scss b/styles/src/components/_slider.scss index a330a46..4a29c7a 100755 --- a/styles/src/components/_slider.scss +++ b/styles/src/components/_slider.scss @@ -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; + } + } } \ No newline at end of file