forked from Hexworks/zircon
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request Hexworks#373 from nanodeath/scrollable-controls
Vertical Scrollable List Fragment
- Loading branch information
Showing
10 changed files
with
503 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
zircon.core/src/commonMain/kotlin/org/hexworks/zircon/api/fragment/ScrollableList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T> : Fragment { | ||
val items: List<T> | ||
fun scrollTo(idx: Int) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
...rc/commonMain/kotlin/org/hexworks/zircon/internal/fragment/impl/VerticalScrollableList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T>( | ||
private val size: Size, | ||
position: Position, | ||
override val items: List<T>, | ||
/** 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<ScrollBar>? = null | ||
) : ScrollableList<T> { | ||
/** Reusable list of labels we display in the main scroll panel. */ | ||
private val labels = mutableListOf<Label>() | ||
|
||
/** Index in [items] of the top item we're showing in the main scroll panel. */ | ||
private var topItemIdx: Int = 0 | ||
|
||
override val root = Components.hbox() | ||
.withSize(size) | ||
.withPosition(position) | ||
.withSpacing(0) | ||
.build() | ||
|
||
private val scrollPanel = Components.vbox() | ||
.withSize(size.withRelativeWidth(-1)) | ||
.withDecorations() | ||
.withSpacing(0) | ||
.build() | ||
|
||
private val scrollBarVbox = Components.vbox() | ||
.withSize(size.withWidth(1)) | ||
.withDecorations() | ||
.withSpacing(0) | ||
.build() | ||
|
||
private val actualScrollbar: ScrollBar = Components.verticalScrollbar() | ||
.withSize(1, size.height - 2) | ||
.withItemsShownAtOnce(size.height) | ||
.withNumberOfScrollableItems(items.size) | ||
.withDecorations() | ||
.also { builder -> | ||
scrollbarRenderer?.let { builder.withComponentRenderer(it) } | ||
} | ||
.build() | ||
|
||
private val decrementButton = Components.button() | ||
.withText("${Symbols.TRIANGLE_UP_POINTING_BLACK}") | ||
.withSize(1, 1) | ||
.withDecorations() | ||
.build() | ||
|
||
private val incrementButton = Components.button() | ||
.withText("${Symbols.TRIANGLE_DOWN_POINTING_BLACK}") | ||
.withSize(1, 1) | ||
.withDecorations() | ||
.build() | ||
|
||
init { | ||
root.addComponents(scrollPanel, scrollBarVbox) | ||
|
||
decrementButton.processComponentEvents(ComponentEventType.ACTIVATED) { | ||
actualScrollbar.decrementValues() | ||
} | ||
incrementButton.processComponentEvents(ComponentEventType.ACTIVATED) { | ||
actualScrollbar.incrementValues() | ||
} | ||
|
||
actualScrollbar.onValueChange { | ||
scrollTo(it.newValue) | ||
} | ||
|
||
scrollBarVbox.addComponents(decrementButton, actualScrollbar, incrementButton) | ||
|
||
displayListFromIndex() | ||
} | ||
|
||
override fun scrollTo(idx: Int) { | ||
topItemIdx = idx | ||
displayListFromIndex() | ||
} | ||
|
||
private fun displayListFromIndex() { | ||
val maxIdx = when { | ||
topItemIdx + size.height < items.size -> topItemIdx + size.height | ||
else -> items.size | ||
} | ||
for (idx in topItemIdx until maxIdx) { | ||
val labelIdx = idx - topItemIdx | ||
// Generate and add labels until we have enough for the current entry | ||
while (labelIdx > labels.lastIndex) { | ||
labels.add(Components.label() | ||
.withDecorations() | ||
.withSize(scrollPanel.contentSize.withHeight(1)) | ||
.build() | ||
.also { label -> | ||
scrollPanel.addComponent(label) | ||
label.onActivated { | ||
onItemActivated(items[topItemIdx + labelIdx], topItemIdx + labelIdx) | ||
} | ||
} | ||
) | ||
} | ||
labels[labelIdx].text = renderItem(items[idx]) | ||
} | ||
// Clear any remaining labels, just in case | ||
for (labelIdx in (maxIdx - topItemIdx) until labels.size) { | ||
labels[labelIdx].text = "" | ||
} | ||
} | ||
} |
79 changes: 79 additions & 0 deletions
79
zircon.core/src/commonTest/kotlin/org/hexworks/zircon/internal/renderer/TestRenderer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package org.hexworks.zircon.internal.renderer | ||
|
||
import org.hexworks.cobalt.databinding.api.extension.toProperty | ||
import org.hexworks.cobalt.databinding.api.property.Property | ||
import org.hexworks.cobalt.databinding.api.value.ObservableValue | ||
import org.hexworks.zircon.api.behavior.Clearable | ||
import org.hexworks.zircon.api.component.ComponentContainer | ||
import org.hexworks.zircon.api.data.Size | ||
import org.hexworks.zircon.api.graphics.TileGraphics | ||
import org.hexworks.zircon.api.grid.TileGrid | ||
import org.hexworks.zircon.api.resource.TilesetResource | ||
import org.hexworks.zircon.api.uievent.UIEvent | ||
import org.hexworks.zircon.api.uievent.UIEventResponse | ||
import org.hexworks.zircon.api.view.base.BaseView | ||
import org.hexworks.zircon.internal.behavior.RenderableContainer | ||
import org.hexworks.zircon.internal.config.RuntimeConfig | ||
import org.hexworks.zircon.internal.graphics.FastTileGraphics | ||
import org.hexworks.zircon.internal.grid.ThreadSafeTileGrid | ||
import org.hexworks.zircon.internal.uievent.UIEventDispatcher | ||
|
||
/** | ||
* This is a simple test renderer that draws things back into the provided [tileGraphics]. After instantiation, | ||
* you should call [withComponentContainer] to add components and fragments, and then call [render] to see the result. | ||
* You can also [dispatch] events to interact with it. | ||
* | ||
* @sample org.hexworks.zircon.internal.renderer.TestRendererTest.tinyExample | ||
*/ | ||
class TestRenderer( | ||
private val tileGraphics: TileGraphics, | ||
tileset: TilesetResource = RuntimeConfig.config.defaultTileset, | ||
gridSize: Size = Size.defaultGridSize() | ||
) : UIEventDispatcher, Renderer, Clearable { | ||
private val tileGrid: TileGrid = ThreadSafeTileGrid(tileset, gridSize) | ||
private val mainView = object : BaseView(tileGrid) {} | ||
private val closedValueProperty: Property<Boolean> = false.toProperty() | ||
override val closedValue: ObservableValue<Boolean> get() = closedValueProperty | ||
|
||
init { | ||
mainView.dock() | ||
} | ||
|
||
fun withComponentContainer(cb: ComponentContainer.() -> Unit) { | ||
with(mainView.screen, cb) | ||
} | ||
|
||
override fun create() { | ||
} | ||
|
||
override fun clear() { | ||
tileGraphics.clear() | ||
} | ||
|
||
override fun render() { | ||
(tileGrid as RenderableContainer).renderables.forEach { renderable -> | ||
if (!renderable.isHidden) { | ||
val graphics = FastTileGraphics( | ||
initialSize = renderable.size, | ||
initialTileset = renderable.tileset, | ||
initialTiles = mapOf() | ||
) | ||
renderable.render(graphics) | ||
graphics.contents().forEach { (pos, tile) -> | ||
tileGraphics.draw(tile, pos + renderable.position) | ||
} | ||
} | ||
} | ||
} | ||
|
||
override fun dispatch(event: UIEvent): UIEventResponse = (mainView.screen as UIEventDispatcher).dispatch(event) | ||
|
||
override fun close() { | ||
if (!closedValueProperty.value) { | ||
tileGrid.close() | ||
closedValueProperty.value = true | ||
} | ||
} | ||
|
||
override val isClosed: ObservableValue<Boolean> get() = closedValue | ||
} |
Oops, something went wrong.