Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ You can include the selected **KTX** modules based on the needs of your applicat
| [`ktx-scene2d`](scene2d) | Type-safe Kotlin builders for [`Scene2D`](https://libgdx.com/wiki/graphics/2d/scene2d/scene2d) GUI. |
| [`ktx-script`](script) | Kotlin scripting engine for desktop applications. |
| [`ktx-style`](style) | Type-safe Kotlin builders for `Scene2D` widget styles extending `Skin` API. |
| [`ktx-textratypist`](textratypist) | Type-safe Kotlin builders for [TextraTypist]() widgets. |
Comment thread
michaeloa marked this conversation as resolved.
Outdated
| [`ktx-tiled`](tiled) | Utilities for [Tiled](https://www.mapeditor.org/) maps. |
| [`ktx-vis`](vis) | Type-safe Kotlin builders for [`VisUI`](https://github.com/kotcrab/vis-ui/). |
| [`ktx-vis-style`](vis-style) | Type-safe Kotlin builders for `VisUI` widget styles. |
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/ktx/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const val artemisOdbVersion = "2.3.0"
const val ashleyVersion = "1.7.4"
const val gdxAiVersion = "1.8.2"
const val visUiVersion = "1.5.5"

Comment thread
michaeloa marked this conversation as resolved.
const val textraTypistVersion = "2.1.7"
const val spekVersion = "1.1.5"
const val kotlinTestVersion = "2.0.7"
const val kotlinMockitoVersion = "5.4.0"
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ include(
"scene2d",
"script",
"style",
"textratypist",
"tiled",
"vis",
"vis-style"
Expand Down
66 changes: 66 additions & 0 deletions textratypist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
[![Maven Central](https://img.shields.io/maven-central/v/io.github.libktx/ktx-textratypist.svg)](https://search.maven.org/artifact/io.github.libktx/ktx-textratypist)

# KTX: `TextraTypist` type-safe builders

Utilities for creating [`TextraTypist`](https://github.com/tommyettinger/textratypist) animated text labels using
Kotlin type-safe builders.

## Why?

`TextraTypist` provides animated typewriter-style text rendering for Scene2D.
It’s powerful for dialog boxes, narration, RPG text, terminals, UI hints, and comic-style text effects.

`ktx-textratypist` integrates Textra widgets directly into the existing `ktx-scene2d` DSL.

## Getting Started

```kotlin
import ktx.scene2d.*
import ktx.textratypist.*

val label = typingLabel(text = "Hello, world!")
```

TextraTypist requires a `Skin` with compatible styles for its widgets. This can be handled easily by using a `FWSkin`:

```kotlin
Scene2DSkin.defaultSkin = FWSkin(Gdx.files.internal("my-skin.json"))
```

## Available Builders

| Widget | DSL Method | Description |
Comment thread
michaeloa marked this conversation as resolved.
Outdated
|-------|----------------------------------|--------------------------------------------------|
| `TextraLabel` | `textraLabel(text, skin, style)` | Plain labels with effects and styles |
| `TypingLabel` | `typingLabel(text, skin, style)` | Labels with typing animation, effects and styles |

## Usage Examples

### Basic TypingLabel

```kotlin
scene2d.table {
typingLabel("Welcome to [RED]KTX Textratypist[]!") {
textSpeed = 0.5F
}
}
```

### Using TextraTypist Markup Tags

| Effect | Example Usage |
|-------|--------------------------|
| Color | `[RED]Danger![]` |
| Wave Motion | `{WAVE}Ocean text[]` |
| Slow / Fast typing | `{SLOW}`, `{FAST}`, etc- |
Comment thread
michaeloa marked this conversation as resolved.
Outdated

```kotlin
typingLabel("{WAVE}System reboot[] in [YELLOW]{SLOWER}3... 2... 1...[]")
```

### Synergy

| Library | Purpose |
|--------|---------|
| `ktx-scene2d` | Layout + widget DSL |
| `ktx-actors` | Stage helpers & utilities |
Comment thread
michaeloa marked this conversation as resolved.
Outdated
8 changes: 8 additions & 0 deletions textratypist/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ktx.*

dependencies {
api(project(":scene2d"))
api("com.github.tommyettinger:textratypist:$textraTypistVersion")
testImplementation("com.badlogicgames.gdx:gdx-backend-lwjgl:$gdxVersion")
testImplementation("com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop")
}
2 changes: 2 additions & 0 deletions textratypist/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
projectName=ktx-textratypist
projectDesc=TextraTypist widgets for LibGdx applications.
Comment thread
michaeloa marked this conversation as resolved.
Outdated
56 changes: 56 additions & 0 deletions textratypist/src/main/kotlin/ktx/textratypist/factory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package ktx.textratypist

import com.badlogic.gdx.scenes.scene2d.ui.Skin
import com.github.tommyettinger.textra.TextraLabel
import com.github.tommyettinger.textra.TypingLabel
import ktx.scene2d.KWidget
import ktx.scene2d.Scene2DSkin
import ktx.scene2d.Scene2dDsl
import ktx.scene2d.actor
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

const val DEFAULT_STYLE = "default"
Comment thread
michaeloa marked this conversation as resolved.
Outdated

/**
* @param text will be displayed on the label.
* @param style name of the widget style. Defaults to [DEFAULT_STYLE].
* @param skin [Skin] instance that contains the widget style. Defaults to [Scene2DSkin.defaultSkin].
* @param init will be invoked with the widget as "this". Consumes actor container (usually a [Cell] or [Node]) that
* contains the widget. Might consume the actor itself if this group does not keep actors in dedicated containers.
* Inlined.
* @return a [TextraLabel] instance added to this group.
*/
@Scene2dDsl
@OptIn(ExperimentalContracts::class)
inline fun <S> KWidget<S>.textraLabel(
text: String,
style: String = DEFAULT_STYLE,
skin: Skin = Scene2DSkin.defaultSkin,
init: (@Scene2dDsl TextraLabel).(S) -> Unit = {},
): TextraLabel {
contract { callsInPlace(init, InvocationKind.EXACTLY_ONCE) }
return actor(TextraLabel(text, skin, style), init)
}

/**
* @param text will be displayed on the label.
* @param style name of the widget style. Defaults to [DEFAULT_STYLE].
* @param skin [Skin] instance that contains the widget style. Defaults to [Scene2DSkin.defaultSkin].
* @param init will be invoked with the widget as "this". Consumes actor container (usually a [Cell] or [Node]) that
* contains the widget. Might consume the actor itself if this group does not keep actors in dedicated containers.
* Inlined.
* @return a [TypingLabel] instance added to this group.
*/
@Scene2dDsl
@OptIn(ExperimentalContracts::class)
inline fun <S> KWidget<S>.typingLabel(
text: String,
style: String = DEFAULT_STYLE,
skin: Skin = Scene2DSkin.defaultSkin,
init: (@Scene2dDsl TypingLabel).(S) -> Unit = {},
): TypingLabel {
contract { callsInPlace(init, InvocationKind.EXACTLY_ONCE) }
return actor(TypingLabel(text, skin, style), init)
}
149 changes: 149 additions & 0 deletions textratypist/src/test/kotlin/ktx/textratypist/factoryTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package ktx.textratypist

import com.badlogic.gdx.scenes.scene2d.Actor
import com.badlogic.gdx.utils.Align
import com.github.tommyettinger.textra.TextraLabel
import com.github.tommyettinger.textra.TypingLabel
import ktx.scene2d.*
import org.junit.Assert.*
import org.junit.Test

class BaseFactoryTest : ApplicationTest() {
Comment thread
michaeloa marked this conversation as resolved.
private fun <T : Actor> test(
validate: (T) -> Unit = {},
widget: KWidget<*>.() -> T?,
) {
// Using a parental widget that allows to use invoke actors' factory methods:
val parent = scene2d.table()
// Invoking widget-specific factory method:
val child = parent.widget()
// Ensuring the child is not null and owned by the parent:
assertNotNull(child)
assertTrue(child in parent.children)
validate(child!!) // Additional validation specific to widget.
}

@Test
fun `should be able to create TextraLabel without init block`() =
test(
widget = { textraLabel("TextraTest.") },
validate = {
assertEquals("TextraTest.", it.storedText.toString())
},
)

@Test
fun `should be able to create TypingLabel without init block`() =
test(
widget = { typingLabel("TypingTest.") },
validate = {
assertEquals("TypingTest.", it.storedText.toString())
},
)

@Test
fun `should be able to create TextraLabel with init block`() =
test(
widget = { textraLabel("TextraTest.") { alignment = Align.bottom } },
validate = {
assertEquals("TextraTest.", it.storedText.toString())
assertEquals(Align.bottom, it.alignment)
},
)

@Test
fun `should be able to create TypingLabel with init block`() =
test(
widget = { typingLabel("TypingTest.") { alignment = Align.bottom } },
validate = {
assertEquals("TypingTest.", it.storedText.toString())
assertEquals(Align.bottom, it.alignment)
},
)

}

class InlinedFactoryTest : ApplicationTest() {
/**
* Tests that textraLabel and typinglabel work within the generic [KWidget] API
*/
@Test
fun `textraLabel within a stack`() {
scene2d.stack {
// In regular groups, children blocks point to the new actor as both 'this' and 'it'.
textraLabel("Actor") {
// No specialized storage objects - 'it' should point to the actor itself:
assertTrue(it is TextraLabel)
assertEquals("Actor", (it as TextraLabel).storedText.toString())
}
}
}

@Test
fun `textraLabel within a table`() {
scene2d.table {
// In table-based groups, children blocks point to the new actor as 'this' and its cell as 'it'.
textraLabel("Cell") {
// Actors stored in cells:
assertTrue(it.actor is TextraLabel)
}
}
}

@Test
fun `textraLabel within a tree`() {
scene2d.tree {
// In trees, children blocks point to the new actor as 'this' and node is the lambda parameter.
textraLabel("Node") { node ->
// Actors stored in tree nodes:
assertTrue(node.actor is TextraLabel)
node {
textraLabel("NestedNode") {
assertTrue(it.actor is TextraLabel)
assertEquals("NestedNode", (it.actor as TextraLabel).storedText.toString())
}
}
}
}
}

@Test
fun `typingLabel within a stack`() {
scene2d.stack {
// In regular groups, children blocks point to the new actor as both 'this' and 'it'.
typingLabel("Actor") {
// No specialized storage objects - 'it' should point to the actor itself:
assertTrue(it is TypingLabel)
assertEquals("Actor", (it as TypingLabel).storedText.toString())
}
}
}

@Test
fun `typingLabel within a table`() {
scene2d.table {
// In table-based groups, children blocks point to the new actor as 'this' and its cell as 'it'.
typingLabel("Cell") {
// Actors stored in cells:
assertTrue(it.actor is TypingLabel)
}
}
}

@Test
fun `typingLabel within a tree`() {
scene2d.tree {
// In trees, children blocks point to the new actor as 'this' and node is the lambda parameter.
typingLabel("Node") { node ->
// Actors stored in tree nodes:
assertTrue(node.actor is TypingLabel)
node {
typingLabel("NestedNode") {
assertTrue(it.actor is TypingLabel)
assertEquals("NestedNode", (it.actor as TypingLabel).storedText.toString())
}
}
}
}
}
}
48 changes: 48 additions & 0 deletions textratypist/src/test/kotlin/ktx/textratypist/testUtilities.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ktx.textratypist

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl.LwjglFiles
import com.badlogic.gdx.backends.lwjgl.LwjglNativesLoader
import com.github.tommyettinger.textra.FWSkin
import ktx.scene2d.Scene2DSkin
import org.junit.AfterClass
import org.junit.BeforeClass
import org.mockito.kotlin.mock

/**
* Utility value for numeric tests.
*/
const val TOLERANCE = 0.0001f

/**
* Tests that require mocked libGDX environment should inherit from this class.
*/
abstract class ApplicationTest {
Comment thread
michaeloa marked this conversation as resolved.
Outdated
companion object {
@JvmStatic
@BeforeClass
fun `initiate libGDX context`() {
LwjglNativesLoader.load()

Gdx.files = LwjglFiles()
Gdx.graphics = mock()
Gdx.gl20 = mock()
Gdx.app = mock()
Gdx.gl = mock()
// Test skin with styles for textratypist widgets:
Scene2DSkin.defaultSkin = FWSkin(Gdx.files.internal("test-skin.json"))
}

@JvmStatic
@AfterClass
fun `destroy libGDX context`() {
Gdx.graphics = null
Gdx.files = null
Gdx.gl20 = null
Gdx.app = null
Gdx.gl = null
Scene2DSkin.defaultSkin.dispose()
}
}

}
Loading