diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 87e694b35..4aaba552f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -45,6 +45,7 @@ import javafx.scene.shape.Rectangle; import javafx.util.Callback; import javafx.util.Duration; import javafx.util.StringConverter; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.ResourceNotFoundError; import org.jackhuang.hmcl.util.i18n.I18n; @@ -452,7 +453,7 @@ public final class FXUtils { derivatives[i] += derivatives[i - 1]; double dy = derivatives[derivatives.length - 1]; double height = listView.getLayoutBounds().getHeight(); - bar.setValue(Math.min(Math.max(bar.getValue() + dy / height, 0), 1)); + bar.setValue(Lang.clamp(0, bar.getValue() + dy / height, 1)); if (Math.abs(dy) < 0.001) timeline.stop(); listView.requestLayout(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java new file mode 100644 index 000000000..4188fe623 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java @@ -0,0 +1,195 @@ +/** + * Hello Minecraft! Launcher + * Copyright (C) 2020 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.construct; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.NumberBinding; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ScrollBar; +import javafx.scene.control.Skin; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import org.jackhuang.hmcl.util.Lang; + +public class FloatScrollBarSkin implements Skin { + private ScrollBar scrollBar; + private Region group; + private Rectangle track = new Rectangle(); + private Rectangle thumb = new Rectangle(); + + public FloatScrollBarSkin(final ScrollBar scrollBar) { + this.scrollBar = scrollBar; + scrollBar.setPrefHeight(1e-18); + scrollBar.setPrefWidth(1e-18); + + this.group = new Region() { + Point2D dragStart; + double preDragThumbPos; + + NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty()); + NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range); + + { + // Children are added unmanaged because for some reason the height of the bar keeps changing + // if they're managed in certain situations... not sure about the cause. + getChildren().addAll(track, thumb); + + track.setManaged(false); + track.getStyleClass().add("track"); + + thumb.setManaged(false); + thumb.getStyleClass().add("thumb"); + + scrollBar.orientationProperty().addListener(obs -> setup()); + + setup(); + + + thumb.setOnMousePressed(me -> { + if (me.isSynthesized()) { + // touch-screen events handled by Scroll handler + me.consume(); + return; + } + /* + ** if max isn't greater than min then there is nothing to do here + */ + if (getSkinnable().getMax() > getSkinnable().getMin()) { + dragStart = thumb.localToParent(me.getX(), me.getY()); + double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax()); + preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin()); + me.consume(); + } + }); + + + thumb.setOnMouseDragged(me -> { + if (me.isSynthesized()) { + // touch-screen events handled by Scroll handler + me.consume(); + return; + } + /* + ** if max isn't greater than min then there is nothing to do here + */ + if (getSkinnable().getMax() > getSkinnable().getMin()) { + /* + ** if the tracklength isn't greater then do nothing.... + */ + if (trackLength() > thumbLength()) { + Point2D cur = thumb.localToParent(me.getX(), me.getY()); + if (dragStart == null) { + // we're getting dragged without getting a mouse press + dragStart = thumb.localToParent(me.getX(), me.getY()); + } + double dragPos = getSkinnable().getOrientation() == Orientation.VERTICAL ? cur.getY() - dragStart.getY(): cur.getX() - dragStart.getX(); + double position = preDragThumbPos + dragPos / (trackLength() - thumbLength()); + if (!getSkinnable().isFocused() && getSkinnable().isFocusTraversable()) getSkinnable().requestFocus(); + double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin(); + if (!Double.isNaN(newValue)) { + getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax())); + } + } + + me.consume(); + } + }); + } + + private double trackLength() { + return getSkinnable().getOrientation() == Orientation.VERTICAL ? track.getHeight() : track.getWidth(); + } + + private double thumbLength() { + return getSkinnable().getOrientation() == Orientation.VERTICAL ? thumb.getHeight() : thumb.getWidth(); + } + + private double boundedSize(double min, double value, double max) { + return Math.min(Math.max(value, min), Math.max(min, max)); + } + + private void setup() { + track.widthProperty().unbind(); + track.heightProperty().unbind(); + + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + track.relocate(0, -5); + track.widthProperty().bind(scrollBar.widthProperty()); + track.setHeight(5); + } else { + track.relocate(-5, 0); + track.setWidth(5); + track.heightProperty().bind(scrollBar.heightProperty()); + } + + thumb.xProperty().unbind(); + thumb.yProperty().unbind(); + thumb.widthProperty().unbind(); + thumb.heightProperty().unbind(); + + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + thumb.relocate(0, -5); + thumb.widthProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty()))); + thumb.setHeight(5); + thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position)); + } else { + thumb.relocate(-5, 0); + thumb.setWidth(5); + thumb.heightProperty().bind(Bindings.max(5, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty()))); + thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position)); + } + } + + @Override + protected double computeMaxWidth(double height) { + if (scrollBar.getOrientation() == Orientation.HORIZONTAL) { + return Double.MAX_VALUE; + } + + return 5; + } + + @Override + protected double computeMaxHeight(double width) { + if (scrollBar.getOrientation() == Orientation.VERTICAL) { + return Double.MAX_VALUE; + } + + return 5; + } + }; + } + + @Override + public void dispose() { + scrollBar = null; + group = null; + } + + @Override + public Node getNode() { + return group; + } + + @Override + public ScrollBar getSkinnable() { + return scrollBar; + } +} diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index cbaf23a66..e8c5a4da2 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -18,6 +18,30 @@ .root { } +.scroll-bar { + -fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin"; +} + +.scroll-bar .track { + -fx-stroke-width: 1; + -fx-stroke: -c-dark-glass; + -fx-arc-width: 5px; + -fx-arc-height: 5px; + -fx-fill: transparent; +} + +.scroll-bar .thumb { + -fx-fill: rgba(255, 255, 255, 0.5); + -fx-arc-width: 5px; + -fx-arc-height: 5px; +} + +.list-view, +.scroll-pane, +.scroll-pane > .viewport { + -fx-background-color: transparent; +} + .disabled Label { -fx-text-fill: rgba(0, 0, 0, 0.5); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 9e2df62a1..16d552f41 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -58,6 +58,20 @@ public final class Lang { return Collections.unmodifiableList(Arrays.asList(elements)); } + public static > T clamp(T min, T val, T max) { + if (val.compareTo(min) < 0) return min; + else if (val.compareTo(max) > 0) return max; + else return val; + } + + public static double clamp(double min, double val, double max) { + return Math.max(min, Math.min(val, max)); + } + + public static int clamp(int min, int val, int max) { + return Math.max(min, Math.min(val, max)); + } + public static boolean test(ExceptionalRunnable r) { try { r.run();