diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/component/VerticalScrollBarBuilder.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/component/VerticalScrollBarBuilder.kt index a4a31706f8..0d58a2bcdb 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/component/VerticalScrollBarBuilder.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/builder/component/VerticalScrollBarBuilder.kt @@ -19,17 +19,24 @@ class VerticalScrollBarBuilder( private var maxValue: Int = 100 ) : BaseComponentBuilder(VerticalScrollBarRenderer()) { + private var itemsShownAtOnce: Int? = null + fun withNumberOfScrollableItems(items: Int) = also { require(items > 0) { "Number of items must be greater than 0." } this.maxValue = items } + fun withItemsShownAtOnce(count: Int) = also { + require(count > 0) { "Count must be greater than 0." } + this.itemsShownAtOnce = count + } + override fun build(): ScrollBar = DefaultVerticalScrollBar( componentMetadata = createMetadata(), renderingStrategy = createRenderingStrategy(), minValue = minValue, maxValue = maxValue, - itemsShownAtOnce = size.height, + itemsShownAtOnce = itemsShownAtOnce ?: size.height, numberOfSteps = size.height, ) diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/fragment/ScrollableList.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/fragment/ScrollableList.kt new file mode 100644 index 0000000000..71ed3b703c --- /dev/null +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/fragment/ScrollableList.kt @@ -0,0 +1,10 @@ +package org.hexworks.zircon.api.fragment + +import org.hexworks.zircon.api.Beta +import org.hexworks.zircon.api.component.Fragment + +@Beta +interface ScrollableList : Fragment { + val items: List + fun scrollTo(idx: Int) +} diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/BaseScrollBar.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/BaseScrollBar.kt index a4d7503b61..626a2aedc1 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/BaseScrollBar.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/BaseScrollBar.kt @@ -59,11 +59,7 @@ abstract class BaseScrollBar( } private fun computeCurrentStep(newValue: Int) { - val actualValue = when { - newValue > maxValue -> maxValue - newValue < minValue -> minValue - else -> newValue - } + val actualValue = newValue.coerceIn(minValue..maxValue) val actualStep = actualValue.toDouble() / valuePerStep val roundedStep = truncate(actualStep) currentStep = roundedStep.toInt() @@ -84,6 +80,9 @@ abstract class BaseScrollBar( override fun incrementStep() { if (currentStep + barSizeInSteps < numberOfSteps) { computeValueToClosestOfStep(currentStep + 1) + } else { + // Try to increase by partial step, so we can always get to the last partial page of items + incrementValues() } } @@ -168,6 +167,20 @@ abstract class BaseScrollBar( } else Pass } + override fun mouseWheelRotatedUp(event: MouseEvent, phase: UIEventPhase): UIEventResponse { + if (phase != UIEventPhase.TARGET) return Pass + val originalValue = currentValue + decrementStep() + return if (currentValue != originalValue) Processed else Pass + } + + override fun mouseWheelRotatedDown(event: MouseEvent, phase: UIEventPhase): UIEventResponse { + if (phase != UIEventPhase.TARGET) return Pass + val originalValue = currentValue + incrementStep() + return if (currentValue != originalValue) Processed else Pass + } + abstract override fun keyPressed(event: KeyboardEvent, phase: UIEventPhase): UIEventResponse override fun activated() = whenEnabledRespondWith { diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/DefaultVerticalScrollBar.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/DefaultVerticalScrollBar.kt index 4e30cea77b..f8fb9f1bc9 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/DefaultVerticalScrollBar.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/impl/DefaultVerticalScrollBar.kt @@ -31,11 +31,11 @@ class DefaultVerticalScrollBar( if (phase == UIEventPhase.TARGET) { when (event.code) { KeyCode.UP -> { - incrementValues() + decrementValues() Processed } KeyCode.DOWN -> { - decrementValues() + incrementValues() Processed } KeyCode.RIGHT -> { diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/renderer/VerticalScrollBarRenderer.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/renderer/VerticalScrollBarRenderer.kt index 7f19fa2770..88d43a7557 100644 --- a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/renderer/VerticalScrollBarRenderer.kt +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/component/renderer/VerticalScrollBarRenderer.kt @@ -8,10 +8,12 @@ import org.hexworks.zircon.api.data.Position import org.hexworks.zircon.api.data.Tile import org.hexworks.zircon.api.graphics.TileGraphics -@Suppress("DuplicatedCode") -class VerticalScrollBarRenderer : ComponentRenderer { +open class VerticalScrollBarRenderer internal constructor() : ComponentRenderer { + open val aboveBarCharacter: Char = ' ' + open val belowBarCharacter: Char = ' ' + open val barCharacter: Char = ' ' - override fun render(tileGraphics: TileGraphics, context: ComponentRenderContext) { + final override fun render(tileGraphics: TileGraphics, context: ComponentRenderContext) { val defaultStyleSet = context.componentStyle.fetchStyleFor(ComponentState.DEFAULT) val invertedDefaultStyleSet = defaultStyleSet .withBackgroundColor(defaultStyleSet.foregroundColor) @@ -24,18 +26,18 @@ class VerticalScrollBarRenderer : ComponentRenderer { tileGraphics.applyStyle(context.currentStyle) - (0..totalScrollBarHeight).forEach { idx -> + (0 until totalScrollBarHeight).forEach { idx -> when { idx < lowBarPosition -> tileGraphics.draw( - Tile.createCharacterTile(' ', disabledStyleSet), + Tile.createCharacterTile(aboveBarCharacter, disabledStyleSet), Position.create(0, idx) ) idx > highBarPosition -> tileGraphics.draw( - Tile.createCharacterTile(' ', disabledStyleSet), + Tile.createCharacterTile(belowBarCharacter, disabledStyleSet), Position.create(0, idx) ) else -> tileGraphics.draw( - Tile.createCharacterTile(' ', invertedDefaultStyleSet), + Tile.createCharacterTile(barCharacter, invertedDefaultStyleSet), Position.create(0, idx) ) } diff --git a/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/fragment/impl/VerticalScrollableList.kt b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/fragment/impl/VerticalScrollableList.kt new file mode 100644 index 0000000000..20be78596f --- /dev/null +++ b/zircon.core/src/commonMain/kotlin/org/hexworks/zircon/internal/fragment/impl/VerticalScrollableList.kt @@ -0,0 +1,138 @@ +package org.hexworks.zircon.internal.fragment.impl + +import org.hexworks.zircon.api.Components +import org.hexworks.zircon.api.component.Label +import org.hexworks.zircon.api.component.ScrollBar +import org.hexworks.zircon.api.component.renderer.ComponentRenderer +import org.hexworks.zircon.api.data.Position +import org.hexworks.zircon.api.data.Size +import org.hexworks.zircon.api.fragment.ScrollableList +import org.hexworks.zircon.api.graphics.Symbols +import org.hexworks.zircon.api.uievent.ComponentEventType + +/** + * This creates a vertically-scrolling list. You provide a list of [items] and a subset of them are rendered with + * a scrollbar on the right side. + * + * ### Navigation + * * To scroll by **single row**, you can click or activate the top/bottom arrows. Arrow keys while the bar is focused also + * works. + * * To scroll by **step** (small jumps), you can click on the empty parts above or below the bar, or use the mouse wheel. + * * You can also click and drag the bar itself. + * + * ### Limitations + * * [items] is immutable. You can't change the list after its created. + * * [items] aren't focusable. You can click on them, but you can't tab to them. + * * Each [item][items] can't span multiple lines. It will be clipped if it's too long. + * * Even if [items] fully fits in [size], a scrollbar will still be displayed. + */ +class VerticalScrollableList( + private val size: Size, + position: Position, + override val items: List, + /** Handler for when an item in the list is activated. */ + private val onItemActivated: (item: T, idx: Int) -> Unit = { _, _ -> }, + /** Transform items in [items] into displayable strings. */ + private val renderItem: (T) -> String = { it.toString() }, + /** If set, use this instead of the default [ComponentRenderer] for the [ScrollBar] created internally. */ + private val scrollbarRenderer: ComponentRenderer? = null +) : ScrollableList { + /** Reusable list of labels we display in the main scroll panel. */ + private val labels = mutableListOf