Skip to content

Commit

Permalink
Merge pull request #3 from arkivanov/settings
Browse files Browse the repository at this point in the history
Added settings dialog
  • Loading branch information
arkivanov authored Feb 5, 2024
2 parents c170ce8 + f91143d commit 8782cd1
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 28 deletions.
15 changes: 0 additions & 15 deletions composeApp/src/commonMain/kotlin/com/arkivanov/minesweeper/App.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.arkivanov.minesweeper

import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.children.ChildNavState
import com.arkivanov.decompose.router.children.NavState
import com.arkivanov.decompose.router.children.NavigationSource
import com.arkivanov.decompose.router.children.SimpleChildNavState
import com.arkivanov.decompose.router.children.children
import com.arkivanov.decompose.value.Value
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable

internal fun <C : Any, T : Any> ComponentContext.child(
source: NavigationSource<C>,
serializer: KSerializer<C>,
initialConfiguration: () -> C,
key: String = "child",
childFactory: (C, ComponentContext) -> T,
): Value<T> =
children(
source = source,
stateSerializer = SimpleNavState.serializer(typeSerial0 = serializer),
initialState = { SimpleNavState(initialConfiguration()) },
key = key,
navTransformer = { _, config -> SimpleNavState(config) },
stateMapper = { _, children -> requireNotNull(children.single().instance) },
childFactory = childFactory,
)

@Serializable
private data class SimpleNavState<C : Any>(
private val config: C,
) : NavState<C> {
override val children: List<ChildNavState<C>> =
listOf(SimpleChildNavState(config, ChildNavState.Status.RESUMED))
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.arkivanov.minesweeper.game

import kotlinx.serialization.Serializable

@Serializable
internal data class GameSettings(
val width: Int,
val height: Int,
val maxMines: Int,
val width: Int = 20,
val height: Int = 20,
val maxMines: Int = 30,
)
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
package com.arkivanov.minesweeper.root

import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.childContext
import com.arkivanov.decompose.router.children.SimpleNavigation
import com.arkivanov.decompose.router.slot.ChildSlot
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.decompose.value.Value
import com.arkivanov.minesweeper.child
import com.arkivanov.minesweeper.game.DefaultGameComponent
import com.arkivanov.minesweeper.game.GameComponent
import com.arkivanov.minesweeper.game.GameSettings
import com.arkivanov.minesweeper.settings.DefaultEditSettingsComponent
import com.arkivanov.minesweeper.settings.EditSettingsComponent
import com.arkivanov.mvikotlin.core.store.StoreFactory

internal class DefaultRootComponent(
componentContext: ComponentContext,
gameComponentFactory: GameComponent.Factory,
editSettingsComponentFactory: EditSettingsComponent.Factory,
) : RootComponent, ComponentContext by componentContext {

override val gameComponent: GameComponent =
gameComponentFactory(
componentContext = childContext(key = "game"),
settings = GameSettings(width = 20, height = 20, maxMines = 30),
private var settings = GameSettings()

private val gameNav = SimpleNavigation<GameSettings>()
override val gameComponent: Value<GameComponent> =
child(
source = gameNav,
serializer = GameSettings.serializer(),
initialConfiguration = { settings },
childFactory = { settings, ctx -> gameComponentFactory(componentContext = ctx, settings = settings) },
)

private val editSettingsNav = SlotNavigation<GameSettings>()
override val editSettingsComponent: Value<ChildSlot<*, EditSettingsComponent>> =
childSlot(
source = editSettingsNav,
serializer = null,
childFactory = { settings, _ ->
editSettingsComponentFactory(
settings = settings,
onConfirmed = {
this.settings = it
editSettingsNav.dismiss()
gameNav.navigate(it)
},
onCancelled = editSettingsNav::dismiss,
)
},
)

override fun onEditSettingsClicked() {
editSettingsNav.activate(settings)
}
}

internal fun DefaultRootComponent(componentContext: ComponentContext, storeFactory: StoreFactory): DefaultRootComponent =
Expand All @@ -25,4 +62,5 @@ internal fun DefaultRootComponent(componentContext: ComponentContext, storeFacto
gameComponentFactory = { ctx, settings ->
DefaultGameComponent(componentContext = ctx, storeFactory = storeFactory, settings = settings)
},
editSettingsComponentFactory = ::DefaultEditSettingsComponent,
)
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.arkivanov.minesweeper.root

import com.arkivanov.decompose.router.slot.ChildSlot
import com.arkivanov.decompose.value.Value
import com.arkivanov.minesweeper.game.GameComponent
import com.arkivanov.minesweeper.settings.EditSettingsComponent

internal interface RootComponent {

val gameComponent: GameComponent
val gameComponent: Value<GameComponent>
val editSettingsComponent: Value<ChildSlot<*, EditSettingsComponent>>

fun onEditSettingsClicked()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.arkivanov.minesweeper.root

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.minesweeper.game.GameContent
import com.arkivanov.minesweeper.settings.EditSettingsContent

@Composable
internal fun RootContent(component: RootComponent) {
val gameComponent by component.gameComponent.subscribeAsState()
val editSettingsComponentSlot by component.editSettingsComponent.subscribeAsState()

MaterialTheme {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
title = { Text("Minesweeper") },
actions = {
IconButton(onClick = component::onEditSettingsClicked) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
)
}
},
)

GameContent(component = gameComponent, modifier = Modifier.fillMaxSize())
}

editSettingsComponentSlot.child?.instance?.also { editSettingsComponent ->
EditSettingsContent(component = editSettingsComponent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.arkivanov.minesweeper.settings

import com.arkivanov.decompose.value.MutableValue
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.update
import com.arkivanov.minesweeper.game.GameSettings
import com.arkivanov.minesweeper.settings.EditSettingsComponent.Model

internal class DefaultEditSettingsComponent(
settings: GameSettings,
private val onConfirmed: (GameSettings) -> Unit,
private val onDismissed: () -> Unit,
) : EditSettingsComponent {

private val _model =
MutableValue(
Model(
width = settings.width.toString(),
height = settings.height.toString(),
maxMines = settings.maxMines.toString(),
)
)

override val model: Value<Model> = _model

override fun onWidthChanged(text: String) {
_model.update { it.copy(width = text) }
}

override fun onHeightChanged(text: String) {
_model.update { it.copy(height = text) }
}

override fun onMaxMinesChanged(text: String) {
_model.update { it.copy(maxMines = text) }
}

override fun onConfirmClicked() {
val width = _model.value.width.toIntOrNull() ?: return
val height = _model.value.height.toIntOrNull() ?: return
val maxMines = _model.value.maxMines.toIntOrNull() ?: return

onConfirmed(
GameSettings(
width = width,
height = height,
maxMines = maxMines,
)
)
}

override fun onDismissRequested() {
onDismissed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.arkivanov.minesweeper.settings

import com.arkivanov.decompose.value.Value
import com.arkivanov.minesweeper.game.GameSettings

internal interface EditSettingsComponent {

val model: Value<Model>

fun onWidthChanged(text: String)
fun onHeightChanged(text: String)
fun onMaxMinesChanged(text: String)
fun onConfirmClicked()
fun onDismissRequested()

data class Model(
val width: String,
val height: String,
val maxMines: String,
)

fun interface Factory {
operator fun invoke(
settings: GameSettings,
onConfirmed: (GameSettings) -> Unit,
onCancelled: () -> Unit,
): EditSettingsComponent
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.arkivanov.minesweeper.settings

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.arkivanov.decompose.extensions.compose.subscribeAsState

@Composable
internal fun EditSettingsContent(component: EditSettingsComponent) {
val model by component.model.subscribeAsState()

Dialog(onDismissRequest = component::onDismissRequested) {
Surface(shape = MaterialTheme.shapes.medium) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
TextField(
value = model.width,
onValueChange = component::onWidthChanged,
label = { Text(text = "Width") },
)

TextField(
value = model.height,
onValueChange = component::onHeightChanged,
label = { Text(text = "Height") },
)

TextField(
value = model.maxMines,
onValueChange = component::onMaxMinesChanged,
label = { Text(text = "Mine count") },
)

Row(
modifier = Modifier.align(Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Button(onClick = component::onConfirmClicked) {
Text(text = "Apply")
}

Button(onClick = component::onConfirmClicked) {
Text(text = "Cancel")
}
}
}
}
}
}
4 changes: 2 additions & 2 deletions composeApp/src/jvmMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.lifecycle.LifecycleController
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.minesweeper.App
import com.arkivanov.minesweeper.root.DefaultRootComponent
import com.arkivanov.minesweeper.root.RootContent
import com.arkivanov.mvikotlin.timetravel.server.TimeTravelServer
import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory
import javax.swing.SwingUtilities
Expand All @@ -27,7 +27,7 @@ fun main() {
val windowState = rememberWindowState()

Window(onCloseRequest = ::exitApplication, title = "Minesweeper", state = windowState) {
App(component = root)
RootContent(component = root)
}

@OptIn(ExperimentalDecomposeApi::class)
Expand Down
4 changes: 2 additions & 2 deletions composeApp/src/wasmJsMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import androidx.compose.ui.window.CanvasBasedWindow
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.resume
import com.arkivanov.minesweeper.App
import com.arkivanov.minesweeper.root.DefaultRootComponent
import com.arkivanov.minesweeper.root.RootContent
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory

@OptIn(ExperimentalComposeUiApi::class)
Expand All @@ -20,6 +20,6 @@ fun main() {
lifecycle.resume()

CanvasBasedWindow(title = "Minesweeper", canvasElementId = "ComposeTarget") {
App(root)
RootContent(root)
}
}

0 comments on commit 8782cd1

Please sign in to comment.