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

Improve SceneContainer.changeTo, added SceneContainer.navigationEntries and document navigation API #1705

Merged
Show file tree
Hide file tree
Changes from all commits
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
48 changes: 43 additions & 5 deletions docs/korge/reference/scene/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ fa-icon: fa-images
priority: 20
---

While you can create a small application in a few lines, when the application grows in size, you will want
to follow some patterns to be able to split your application effectively.
While you can create a small application in a few lines, when the application grows in size,
you will want to follow some patterns to be able to split your application effectively.

KorGE includes an asynchronous dependency injector and some tools like the modules and scenes to do so.

Expand All @@ -32,7 +32,8 @@ class MyScene : Scene() {
}
```

For simplicity, it is possible to only provide one of those methods. You can for example only use sceneMain in simple cases:
For simplicity, it is possible to only provide one of those methods.
You can for example only use sceneMain in simple cases:

```kotlin
class MyScene : Scene() {
Expand All @@ -44,7 +45,8 @@ class MyScene : Scene() {

### SceneContainer

`Scene`s must be added to a `SceneContainer`. `SceneContainer` is a `View`, so it can be attached to the scene graph.
`Scene`s must be added to a `SceneContainer`. `SceneContainer` is a `View`,
so it can be attached to the scene graph.

```kotlin
fun main() = Korge {
Expand All @@ -56,7 +58,8 @@ fun main() = Korge {

### Changing to a Scene

Once you have the SceneContainer view attached to the stage/scene graph, you can change it to actually show a specific Scene:
Once you have the SceneContainer view attached to the stage/scene graph,
you can change it to actually show a specific Scene:

```kotlin
sceneContainer.changeTo({ MyScene() })
Expand Down Expand Up @@ -115,6 +118,19 @@ data class MyScene(val service: MyService, val params: MySceneParameters)
sceneContainer.changeTo<MyScene>(MySceneParameters("mylevelname", "myid"))
```

### Changing to another scene with same SceneContainer inside a Scene

Scenes have a `sceneContainer` reference where it has been loaded.
So it is possible do:

```kotlin
fun SContainer.sceneMain() {
uiButton("Change") {
onClick { sceneContainer.changeTo { AnotherScene() } }
}
}
```

## Scene lifecycle

In addition to `sceneInit` and `sceneMain`, Scenes provide other methods you can override:
Expand Down Expand Up @@ -241,3 +257,25 @@ val TransitionFilter.Transition.DIAGONAL2
val TransitionFilter.Transition.CIRCULAR
val TransitionFilter.Transition.SWEEP
```

## Navigation API

In addition to changing the scene, it is also possible to have a
navigation stack of scenes we can go forward and back. For example:

```kotlin
class AnotherScene1(val params: MySceneParameters) : Scene {}
class AnotherScene2(val params: MySceneParameters) : Scene {}
injector.mapPrototype { AnotherScene1(get()) }
injector.mapPrototype { AnotherScene2(get()) }

sceneContainer.pushTo<AnotherScene1>(MySceneParameters()) // AnotherScene1
sceneContainer.pushTo<AnotherScene2>(MySceneParameters()) // AnotherScene2
sceneContainer.back() // AnotherScene1
sceneContainer.forward() // AnotherScene2

println(sceneContainer.navigationEntries) // List<SceneContainer.VisitEntry>
```

This API is only available along the dependency injector.
Since `back` and `forward` need to know how to construct and recreate the scenes from the injected parameters.
6 changes: 3 additions & 3 deletions korge-sandbox/src/commonMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,9 @@ suspend fun Stage.demoSelector(default: Demo, all: List<Demo>) {
if (demo != null) {
comboBox.selectedItem = demo
views.clearColor = DEFAULT_KORGE_BG_COLOR
container.changeTo({ injector ->
demo.sceneBuilder().also { it.init(injector) }
})
container.changeTo {
demo.sceneBuilder().also { it.init(this) }
}
}
}

Expand Down
44 changes: 35 additions & 9 deletions korge/src/commonMain/kotlin/korlibs/korge/scene/SceneContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class SceneContainer(
} as KClass<Scene>

val scene = changeTo(sceneClass, {
it.get(sceneClass)
get(sceneClass)
.also { newScene ->
try {
event.transferKeepProperties(scene, newScene)
Expand Down Expand Up @@ -180,11 +180,20 @@ class SceneContainer(
time: TimeSpan = 0.seconds,
transition: Transition = defaultTransition
): T {
visitPos++
setCurrent(VisitEntry(clazz, injects.toList()))
pushEntry(VisitEntry(clazz, injects.toList()))
return _changeTo(clazz, *injects, time = time, transition = transition)
}

//suspend inline fun <reified T : Scene> pushTo(
// vararg injects: Any,
// time: TimeSpan = 0.seconds,
// transition: Transition = defaultTransition,
// crossinline gen: suspend AsyncInjector.() -> T,
//): T {
// pushEntry(VisitEntry(T::class, injects.toList()))
// return changeTo(T::class, gen, injects = injects, time = time, transition = transition, remap = true)
//}

/**
* Changes to the [T] [clazz] [Scene], with a set of optional [injects] instances during [time] time, and with [transition].
* This method waits until the [transition] has been completed, and returns the [T] created instance.
Expand All @@ -199,6 +208,16 @@ class SceneContainer(
return _changeTo(clazz, *injects, time = time, transition = transition)
}

suspend inline fun <reified T : Scene> changeTo(
vararg injects: Any,
time: TimeSpan = 0.seconds,
transition: Transition = defaultTransition,
crossinline gen: suspend AsyncInjector.() -> T,
): T {
return changeTo(T::class, gen, injects = injects, time = time, transition = transition, remap = true)
}

@Deprecated("Use changeTo { ... }")
suspend inline fun <reified T : Scene> changeTo(
crossinline gen: suspend (AsyncInjector) -> T,
vararg injects: Any,
Expand All @@ -210,7 +229,7 @@ class SceneContainer(

suspend inline fun <T : Scene> changeTo(
clazz: KClass<T>,
crossinline gen: suspend (AsyncInjector) -> T,
crossinline gen: suspend AsyncInjector.() -> T,
vararg injects: Any,
time: TimeSpan = 0.seconds,
transition: Transition = defaultTransition,
Expand Down Expand Up @@ -247,7 +266,7 @@ class SceneContainer(
time: TimeSpan = 0.seconds,
transition: Transition = defaultTransition
): T {
return changeTo(clazz, { it.get(clazz) }, *injects, time = time, transition = transition)
return changeTo(clazz, { get(clazz) }, *injects, time = time, transition = transition)
}

/** Check [Scene] for details of the lifecycle. */
Expand Down Expand Up @@ -315,21 +334,28 @@ class SceneContainer(
}


private data class VisitEntry(val clazz: KClass<out Scene>, val injects: List<Any>)
data class VisitEntry(val clazz: KClass<out Scene>, val injects: List<Any>)

companion object {
val logger = Logger("SceneContainer")

private val EMPTY_VISIT_ENTRY = VisitEntry(EmptyScene::class, listOf())
}

private val visitStack = arrayListOf<VisitEntry>(EMPTY_VISIT_ENTRY)
private var visitPos = 0
val navigationEntries: List<VisitEntry> get() = visitStack.toList()

@PublishedApi internal val visitStack = arrayListOf<VisitEntry>(EMPTY_VISIT_ENTRY)
@PublishedApi internal var visitPos = 0

// https://developer.mozilla.org/en/docs/Web/API/History

private fun setCurrent(entry: VisitEntry) {
@PublishedApi internal fun setCurrent(entry: VisitEntry) {
while (visitStack.size <= visitPos) visitStack.add(EMPTY_VISIT_ENTRY)
visitStack[visitPos] = entry
}

@PublishedApi internal fun pushEntry(entry: VisitEntry) {
visitPos++
setCurrent(entry)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ class SceneContainerTest : ViewsForTesting() {
}
}

inner class Scene0(
val info: SceneInfo
) : MyLogScene() {
override val sceneName: String get() = "Scene0"
}

inner class Scene1(
val info: SceneInfo
) : MyLogScene() {
Expand Down Expand Up @@ -88,4 +94,36 @@ class SceneContainerTest : ViewsForTesting() {
val endTime = time
assertTrue { endTime - startTime in 0.5.seconds..0.75.seconds }
}

@Test
fun testChangeToPushTo() = viewsTest {
val sceneContainer = sceneContainer(views)

views.injector.mapPrototype { Scene0(get()) }

assertEquals(listOf(SceneContainer.VisitEntry(EmptyScene::class, emptyList())), sceneContainer.navigationEntries)

sceneContainer.pushTo<Scene0>(SceneInfo("test"))
assertEquals(2, sceneContainer.navigationEntries.size)
assertEquals("test", (sceneContainer.currentScene as Scene0).info.name)

sceneContainer.pushTo<Scene0>(SceneInfo("test2"))
assertEquals(3, sceneContainer.navigationEntries.size)
assertEquals("test2", (sceneContainer.currentScene as Scene0).info.name)

sceneContainer.back()
assertEquals(3, sceneContainer.navigationEntries.size)
assertEquals("test", (sceneContainer.currentScene as Scene0).info.name)

sceneContainer.forward()
assertEquals(3, sceneContainer.navigationEntries.size)
assertEquals("test2", (sceneContainer.currentScene as Scene0).info.name)
}

@Test
fun testNewChangeToSignature() = viewsTest {
val sceneContainer = sceneContainer(views)
val scene = sceneContainer.changeTo { Scene0(SceneInfo("test2")) }
assertEquals(scene, sceneContainer.currentScene)
}
}