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
16 changes: 15 additions & 1 deletion 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,14 +338,17 @@ 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;ZZZFFLgg/essential/elementa/UIComponent;)V
public synthetic fun <init> (Ljava/lang/String;FLjava/awt/Color;Lgg/essential/elementa/components/ScrollComponent$Direction;ZZZFFLgg/essential/elementa/UIComponent;ILkotlin/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
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZ)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZF)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZFF)V
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZFFLgg/essential/elementa/UIComponent;)V
public synthetic fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZFFLgg/essential/elementa/UIComponent;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
public fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZFFLgg/essential/elementa/UIComponent;Z)V
public synthetic fun <init> (Ljava/lang/String;FLjava/awt/Color;ZZZZFFLgg/essential/elementa/UIComponent;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
public fun addChild (Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/components/ScrollComponent;
public final fun addScrollAdjustEvent (ZLkotlin/jvm/functions/Function2;)V
Expand All @@ -360,6 +364,7 @@ public final class gg/essential/elementa/components/ScrollComponent : gg/essenti
public final fun getEmptyText ()Lgg/essential/elementa/components/UIWrappedText;
public final fun getHorizontalOffset ()F
public final fun getHorizontalOverhang ()F
public final fun getMouseScrollLambda ()Lkotlin/jvm/functions/Function2;
public final fun getVerticalOffset ()F
public final fun getVerticalOverhang ()F
public fun hitTest (FF)Lgg/essential/elementa/UIComponent;
Expand Down Expand Up @@ -409,6 +414,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
136 changes: 107 additions & 29 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,64 @@ 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 = Direction.PreferVertical,
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
private val horizontalScrollOpposite: Boolean = false,
private val verticalScrollOpposite: Boolean = false,
private val passthroughScroll: Boolean = true,
private val pixelsPerScroll: Float = 15f,
private val scrollAcceleration: Float = 1.0f,
customScissorBoundingBox: UIComponent? = null
) : 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,
passthroughScroll: Boolean = true
) : 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,
passthroughScroll,
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 +143,45 @@ class ScrollComponent @JvmOverloads constructor(
val verticalOverhang: Float
get() = max(0f, calculateActualHeight() - getHeight())



val mouseScrollLambda: UIComponent.(UIScrollEvent) -> Unit = {
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
if (Window.of(this).version >= ElementaVersion.v5) {
// new behavior
val scrollDirection = if (!UKeyboard.isShiftKeyDown()) primaryScrollDirection else secondaryScrollDirection
scrollDirection?.let { direction ->
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
if (!onScroll(it.delta.toFloat(), isHorizontal = direction == Direction.Horizontal) && passthroughScroll) {
getNextHighestScrollComponent()?.fireScrollEvent(it)
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
}
}

it.stopPropagation()
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved
} 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 +290,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 +432,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
if (newOffset in range) {
changed = true
offset.set(newOffset.coerceIn(range))
}
Johni0702 marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -655,6 +716,16 @@ class ScrollComponent @JvmOverloads constructor(
private fun ClosedFloatingPointRange<Double>.width() = abs(this.start - this.endInclusive)
private fun ClosedFloatingPointRange<Float>.width() = abs(this.start - this.endInclusive)

private fun getNextHighestScrollComponent(): ScrollComponent? {
var current: UIComponent = this.parent

while (current !is ScrollComponent && current.hasParent && current.parent != current) {
current = current.parent
}

return current as? ScrollComponent
}

class DefaultScrollBar(isHorizontal: Boolean) : UIComponent() {
val grip: UIComponent

Expand Down Expand Up @@ -743,6 +814,13 @@ class ScrollComponent @JvmOverloads constructor(

}

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

companion object {

fun getScrollImage(): UIImage {
Expand Down