Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ScrollComponent: Implement better specification of direction #125

Merged
merged 11 commits into from
Oct 19, 2023
12 changes: 12 additions & 0 deletions api/Elementa.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public final class gg/essential/elementa/ElementaVersion : java/lang/Enum {
public static final field V2 Lgg/essential/elementa/ElementaVersion;
public static final field V3 Lgg/essential/elementa/ElementaVersion;
public static final field V4 Lgg/essential/elementa/ElementaVersion;
public static final field V5 Lgg/essential/elementa/ElementaVersion;
public final fun enableFor (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
public static fun valueOf (Ljava/lang/String;)Lgg/essential/elementa/ElementaVersion;
public static fun values ()[Lgg/essential/elementa/ElementaVersion;
Expand Down Expand Up @@ -337,6 +338,8 @@ public final class gg/essential/elementa/components/ScrollComponent : gg/essenti
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;F)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;Lgg/essential/elementa/components/ScrollComponent$Direction;ZZFFLgg/essential/elementa/UIComponent;Z)V
public synthetic fun <init> (Ljava/lang/String;FLjava/awt/Color;Lgg/essential/elementa/components/ScrollComponent$Direction;ZZFFLgg/essential/elementa/UIComponent;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;Z)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZ)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZ)V
Expand Down Expand Up @@ -409,6 +412,15 @@ public final class gg/essential/elementa/components/ScrollComponent$DefaultScrol
public final fun getGrip ()Lgg/essential/elementa/UIComponent;
}

public final class gg/essential/elementa/components/ScrollComponent$Direction : java/lang/Enum {
public static final field Horizontal Lgg/essential/elementa/components/ScrollComponent$Direction;
public static final field PreferHorizontal Lgg/essential/elementa/components/ScrollComponent$Direction;
public static final field PreferVertical Lgg/essential/elementa/components/ScrollComponent$Direction;
public static final field Vertical Lgg/essential/elementa/components/ScrollComponent$Direction;
public static fun valueOf (Ljava/lang/String;)Lgg/essential/elementa/components/ScrollComponent$Direction;
public static fun values ()[Lgg/essential/elementa/components/ScrollComponent$Direction;
}

public final class gg/essential/elementa/components/ScrollComponent$ScrollChildConstraint : gg/essential/elementa/constraints/HeightConstraint, gg/essential/elementa/constraints/WidthConstraint {
public fun <init> (Lgg/essential/elementa/components/ScrollComponent;F)V
public synthetic fun <init> (Lgg/essential/elementa/components/ScrollComponent;FILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
8 changes: 8 additions & 0 deletions src/main/kotlin/gg/essential/elementa/ElementaVersion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ enum class ElementaVersion {
* On prior versions, calling [gg.essential.elementa.UIComponent.enableEffect] on a component that wasn't yet initialized would result
* in the Effect's `beforeDraw` being called once before `setup`.
*/
@Deprecated(DEPRECATION_MESSAGE)
V4,

/**
* Change the behavior of scroll components to no longer require holding down shift when horizontal is the only possible scrolling direction.
*/
V5,

;

/**
Expand Down Expand Up @@ -118,7 +124,9 @@ Be sure to read through all the changes between your current version and your ne
internal val v2 = V2
@Suppress("DEPRECATION")
internal val v3 = V3
@Suppress("DEPRECATION")
internal val v4 = V4
internal val v5 = V5


@PublishedApi
Expand Down
124 changes: 94 additions & 30 deletions src/main/kotlin/gg/essential/elementa/components/ScrollComponent.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package gg.essential.elementa.components

import gg.essential.elementa.ElementaVersion
import gg.essential.elementa.UIComponent
import gg.essential.elementa.constraints.*
import gg.essential.elementa.constraints.animation.Animations
import gg.essential.elementa.constraints.resolution.ConstraintVisitor
import gg.essential.elementa.dsl.*
import gg.essential.elementa.effects.ScissorEffect
import gg.essential.elementa.events.UIScrollEvent
import gg.essential.elementa.utils.bindLast
import gg.essential.universal.UKeyboard
import gg.essential.universal.UMatrixStack
Expand All @@ -20,18 +22,62 @@ import kotlin.math.max
*
* Also prevents scrolling past what should be reasonable.
*/
class ScrollComponent @JvmOverloads constructor(
class ScrollComponent constructor(
emptyString: String = "",
private val innerPadding: Float = 0f,
private val scrollIconColor: Color = Color.WHITE,
private val horizontalScrollEnabled: Boolean = false,
private val verticalScrollEnabled: Boolean = true,
private val scrollDirection: Direction,
private val horizontalScrollOpposite: Boolean = false,
private val verticalScrollOpposite: Boolean = false,
private val pixelsPerScroll: Float = 15f,
private val scrollAcceleration: Float = 1.0f,
customScissorBoundingBox: UIComponent? = null
customScissorBoundingBox: UIComponent? = null,
private val passthroughScroll: Boolean = true
) : UIContainer() {
@JvmOverloads constructor(
emptyString: String = "",
innerPadding: Float = 0f,
scrollIconColor: Color = Color.WHITE,
horizontalScrollEnabled: Boolean = false,
verticalScrollEnabled: Boolean = true,
horizontalScrollOpposite: Boolean = false,
verticalScrollOpposite: Boolean = false,
pixelsPerScroll: Float = 15f,
scrollAcceleration: Float = 1.0f,
customScissorBoundingBox: UIComponent? = null
) : this (
emptyString,
innerPadding,
scrollIconColor,
when {
horizontalScrollEnabled && verticalScrollEnabled -> Direction.PreferVertical
horizontalScrollEnabled && !verticalScrollEnabled -> Direction.Horizontal
!horizontalScrollEnabled && verticalScrollEnabled -> Direction.Vertical
else -> throw IllegalArgumentException("ScrollComponent must have at least one direction of scrolling enabled")
},
horizontalScrollOpposite,
verticalScrollOpposite,
pixelsPerScroll,
scrollAcceleration,
customScissorBoundingBox
)

private val primaryScrollDirection
get() = when (scrollDirection) {
Direction.Horizontal, Direction.PreferHorizontal -> Direction.Horizontal
Direction.Vertical, Direction.PreferVertical -> Direction.Vertical
}
private val secondaryScrollDirection
get() = when (scrollDirection) {
Direction.PreferHorizontal -> Direction.Vertical
Direction.PreferVertical -> Direction.Horizontal
else -> null
}
private val horizontalScrollEnabled
get() = primaryScrollDirection == Direction.Horizontal || secondaryScrollDirection == Direction.Horizontal
private val verticalScrollEnabled
get() = primaryScrollDirection == Direction.Vertical || secondaryScrollDirection == Direction.Vertical

private var animationFPS: Int? = null

private val actualHolder = UIContainer().constrain {
Expand Down Expand Up @@ -95,31 +141,43 @@ class ScrollComponent @JvmOverloads constructor(
val verticalOverhang: Float
get() = max(0f, calculateActualHeight() - getHeight())



private val mouseScrollLambda: UIComponent.(UIScrollEvent) -> Unit = {
if (Window.of(this).version >= ElementaVersion.v5) {
// new behavior
val scrollDirection = if (!UKeyboard.isShiftKeyDown()) primaryScrollDirection else secondaryScrollDirection
if (scrollDirection != null) {
if (onScroll(it.delta.toFloat(), isHorizontal = scrollDirection == Direction.Horizontal) || !passthroughScroll) {
it.stopPropagation()
}
}
} else {
// old behavior
if (UKeyboard.isShiftKeyDown() && horizontalScrollEnabled) {
onScroll(it.delta.toFloat(), isHorizontal = true)
} else if (!UKeyboard.isShiftKeyDown() && verticalScrollEnabled) {
onScroll(it.delta.toFloat(), isHorizontal = false)
}

it.stopPropagation()
}
}

init {
this.constrain {
width = ScrollChildConstraint() coerceAtMost 100.percentOfWindow()
height = ScrollChildConstraint() coerceAtMost 100.percentOfWindow()
}

if (!horizontalScrollEnabled && !verticalScrollEnabled)
throw IllegalArgumentException("ScrollComponent must have at least one direction of scrolling enabled")

super.addChild(actualHolder)
actualHolder.addChild(emptyText)
this.enableEffects(ScissorEffect(customScissorBoundingBox))
emptyText.setFontProvider(getFontProvider())
super.addChild(scrollIconComponent)
scrollIconComponent.hide(instantly = true)

onMouseScroll {
if (UKeyboard.isShiftKeyDown() && horizontalScrollEnabled) {
onScroll(it.delta.toFloat(), isHorizontal = true)
} else if (!UKeyboard.isShiftKeyDown() && verticalScrollEnabled) {
onScroll(it.delta.toFloat(), isHorizontal = false)
}

it.stopPropagation()
}
onMouseScroll(mouseScrollLambda)

onMouseClick { event ->
onClick(event.relativeX, event.relativeY, event.mouseButton)
Expand Down Expand Up @@ -228,15 +286,7 @@ class ScrollComponent @JvmOverloads constructor(
verticalHideScrollWhenUseless = hideWhenUseless
}

component.onMouseScroll {
if (isHorizontal && horizontalScrollEnabled && UKeyboard.isShiftKeyDown()) {
onScroll(it.delta.toFloat(), isHorizontal = true)
} else if (!isHorizontal && verticalScrollEnabled) {
onScroll(it.delta.toFloat(), isHorizontal = false)
}

it.stopPropagation()
}
component.onMouseScroll(mouseScrollLambda)

component.onMouseClick { event ->
if (isHorizontal) {
Expand Down Expand Up @@ -378,17 +428,24 @@ class ScrollComponent @JvmOverloads constructor(
needsUpdate = true
}

private fun onScroll(delta: Float, isHorizontal: Boolean) {
if (isHorizontal) {
horizontalOffset += delta * pixelsPerScroll * currentScrollAcceleration
} else {
verticalOffset += delta * pixelsPerScroll * currentScrollAcceleration
/**
* @return whether the offset changed
*/
private fun onScroll(delta: Float, isHorizontal: Boolean): Boolean {
var changed = false
val offset = if (isHorizontal) ::horizontalOffset else ::verticalOffset
val range = calculateOffsetRange(isHorizontal)
val newOffset = (if(range.isEmpty()) innerPadding else offset.get() + delta * pixelsPerScroll * currentScrollAcceleration).coerceIn(range)
if (newOffset != offset.get()) {
changed = true
offset.set(newOffset)
}

currentScrollAcceleration =
(currentScrollAcceleration + (scrollAcceleration - 1.0f) * 0.15f).coerceIn(0f, scrollAcceleration)

needsUpdate = true
return changed
}

private fun updateScrollBar(scrollPercentage: Float, percentageOfParent: Float, isHorizontal: Boolean) {
Expand Down Expand Up @@ -743,6 +800,13 @@ class ScrollComponent @JvmOverloads constructor(

}

enum class Direction {
Vertical,
Horizontal,
/*BothBut*/PreferVertical,
/*BothBut*/PreferHorizontal,
}

companion object {

fun getScrollImage(): UIImage {
Expand Down