diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/Logger.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/Logger.kt new file mode 100644 index 000000000..434a1b419 --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/Logger.kt @@ -0,0 +1,6 @@ +package com.novoda.gol + +expect object Logger { + + fun log(o: Any?) +} diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppModel.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppModel.kt new file mode 100644 index 000000000..963cb7dc3 --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppModel.kt @@ -0,0 +1,28 @@ +package com.novoda.gol.presentation + +import com.novoda.gol.patterns.PatternEntity +import kotlin.properties.Delegates.observable + +class AppModel { + + private var boardViewState by observable(BoardViewState(true)) { _, _, newValue -> + onBoardStateChanged(newValue) + } + + var onSimulationStateChanged: (isIdle: Boolean) -> Unit by observable<(Boolean) -> Unit>({}) { _, _, newValue -> + newValue(boardViewState.isIdle) + } + + var onBoardStateChanged: (BoardViewState) -> Unit by observable<(BoardViewState) -> Unit>({}) { _, _, newValue -> + newValue(boardViewState) + } + + fun toggleSimulation() { + boardViewState = BoardViewState(isIdle = boardViewState.isIdle.not()) + onSimulationStateChanged(boardViewState.isIdle) + } + + fun selectPattern(pattern: PatternEntity) { + boardViewState = boardViewState.copy(selectedPattern = pattern) + } +} diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppPresenter.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppPresenter.kt new file mode 100644 index 000000000..08f525f7a --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppPresenter.kt @@ -0,0 +1,25 @@ +package com.novoda.gol.presentation + +class AppPresenter { + + private val model = AppModel() + + fun bind(view: AppView) { + + model.onSimulationStateChanged = { isIdle -> + view.renderControlButtonLabel(if (isIdle) "Start simulation" else "Stop Simulation") + view.renderPatternSelectionVisibility(visibility = isIdle) + } + + model.onBoardStateChanged = view::renderBoard + + view.onControlButtonClicked = model::toggleSimulation + + view.onPatternSelected = model::selectPattern + } + + fun unbind(view: AppView) { + model.onSimulationStateChanged = {} + view.onControlButtonClicked = {} + } +} diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppView.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppView.kt new file mode 100644 index 000000000..510570870 --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/AppView.kt @@ -0,0 +1,13 @@ +package com.novoda.gol.presentation + +import com.novoda.gol.patterns.PatternEntity + +interface AppView { + + var onControlButtonClicked : () -> Unit + var onPatternSelected: (pattern : PatternEntity) -> Unit + + fun renderControlButtonLabel(controlButtonLabel: String) + fun renderPatternSelectionVisibility(visibility: Boolean) + fun renderBoard(boardViewState: BoardViewState) +} diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardModelImpl.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardModelImpl.kt index ac09a999c..b8d92b997 100644 --- a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardModelImpl.kt +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardModelImpl.kt @@ -34,7 +34,7 @@ class BoardModelImpl private constructor(initialBoard: BoardEntity, private val } override fun selectPattern(pattern: PatternEntity) { - if (gameLoop.isLooping() || this.pattern == pattern) { + if (gameLoop.isLooping()) { return } this.pattern = pattern diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardViewState.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardViewState.kt new file mode 100644 index 000000000..b27a98f1e --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/BoardViewState.kt @@ -0,0 +1,8 @@ +package com.novoda.gol.presentation + +import com.novoda.gol.patterns.PatternEntity + +data class BoardViewState( + val isIdle: Boolean, + val selectedPattern: PatternEntity? = null +) diff --git a/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/PatternViewState.kt b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/PatternViewState.kt new file mode 100644 index 000000000..4ea6cf003 --- /dev/null +++ b/game-of-life-multiplatform/common/src/main/kotlin/com/novoda/gol/presentation/PatternViewState.kt @@ -0,0 +1,8 @@ +package com.novoda.gol.presentation + +import com.novoda.gol.patterns.PatternEntity + +data class PatternViewState( + var shouldDisplay: Boolean, + val patternEntities: List +) diff --git a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/Logger.kt b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/Logger.kt new file mode 100644 index 000000000..efb463bf3 --- /dev/null +++ b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/Logger.kt @@ -0,0 +1,8 @@ +package com.novoda.gol + +actual object Logger { + + actual fun log(o: Any?) { + console.log(o) + } +} diff --git a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/App.kt b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/App.kt index 82d924f72..ea4e8727a 100644 --- a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/App.kt +++ b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/App.kt @@ -4,16 +4,50 @@ package com.novoda.gol.components import com.novoda.gol.patterns.PatternEntity import com.novoda.gol.patterns.PatternRepository +import com.novoda.gol.presentation.AppPresenter +import com.novoda.gol.presentation.AppView +import com.novoda.gol.presentation.BoardViewState +import com.novoda.gol.presentation.PatternViewState import kotlinx.html.style import react.* import react.dom.div import react.dom.h2 -class App : RComponent() { +class App : RComponent(), AppView { + + override var onControlButtonClicked: () -> Unit = {} + override var onPatternSelected: (pattern: PatternEntity) -> Unit = {} + + private val presenter: AppPresenter = AppPresenter() + + override fun componentWillMount() { + presenter.bind(this) + } + + override fun componentWillUnmount() { + presenter.unbind(this) + } override fun State.init() { - patternEntities = PatternRepository.patterns() - isIdle = true + patternViewState = PatternViewState(true, PatternRepository.patterns()) + } + + override fun renderControlButtonLabel(controlButtonLabel: String) { + setState { + this.controlButtonLabel = controlButtonLabel + } + } + + override fun renderPatternSelectionVisibility(visibility: Boolean) { + setState { + patternViewState.shouldDisplay = visibility + } + } + + override fun renderBoard(boardViewState: BoardViewState) { + setState { + this.boardViewState = boardViewState + } } override fun RBuilder.render(): ReactElement? = @@ -23,10 +57,8 @@ class App : RComponent() { flexDirection = "column" } - controlButton(controlButtonLabel(), { - setState { - isIdle = isIdle.not() - } + controlButton(state.controlButtonLabel, { + onControlButtonClicked() }) div { @@ -34,9 +66,9 @@ class App : RComponent() { display = "flex" } - board(state.isIdle, state.selectedPattern) + board(state.boardViewState) - if (state.isIdle) { + if (state.patternViewState.shouldDisplay) { div { @@ -46,26 +78,21 @@ class App : RComponent() { h2 { +"Choose a pattern" } - for (patternEntity in state.patternEntities) { - pattern(patternEntity, { - setState { - selectedPattern = patternEntity - } + state.patternViewState.patternEntities.forEach { pattern -> + pattern(pattern, { + onPatternSelected(pattern) }) } } } } } - - private fun controlButtonLabel() = if (state.isIdle) "Start simulation" else "Stop Simulation" - } interface State : RState { - var patternEntities: List - var isIdle: Boolean - var selectedPattern: PatternEntity? + var patternViewState: PatternViewState + var boardViewState: BoardViewState + var controlButtonLabel: String } fun RBuilder.app() = child(App::class) {} diff --git a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/Board.kt b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/Board.kt index 1f3275c76..6bac1fca5 100644 --- a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/Board.kt +++ b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/Board.kt @@ -7,6 +7,7 @@ import com.novoda.gol.data.PositionEntity import com.novoda.gol.patterns.PatternEntity import com.novoda.gol.presentation.BoardPresenter import com.novoda.gol.presentation.BoardView +import com.novoda.gol.presentation.BoardViewState import kotlinext.js.js import kotlinx.html.style import react.* @@ -39,18 +40,22 @@ class Board(boardProps: BoardProps) : RComponent(boardPr override fun BoardState.init(props: BoardProps) { presenter = BoardPresenter(50, 50) - if (props.selectedPattern != null) { - onPatternSelected.invoke(props.selectedPattern!!) + if (props.boardViewState.selectedPattern != null) { + onPatternSelected.invoke(props.boardViewState.selectedPattern!!) } } override fun componentWillReceiveProps(nextProps: BoardProps) { - if (nextProps.isIdle.not()) { + val state = nextProps.boardViewState + + if (state.isIdle.not()) { onStartSimulationClicked() } else { onStopSimulationClicked() } - onPatternSelected.invoke(nextProps.selectedPattern!!) + if (state.selectedPattern != null) { + onPatternSelected.invoke(state.selectedPattern!!) + } } override fun RBuilder.render() = renderBoard(state) @@ -74,16 +79,12 @@ class Board(boardProps: BoardProps) : RComponent(boardPr } } -data class BoardProps(var isIdle: Boolean, var selectedPattern: PatternEntity? = null) : RProps +data class BoardProps(var boardViewState: BoardViewState) : RProps interface BoardState : RState { var boardEntity: BoardEntity } -fun RBuilder.board(isIdle: Boolean, selectedPattern: PatternEntity? = null) = child( - Board::class) { - attrs.isIdle = isIdle - attrs.selectedPattern = selectedPattern +fun RBuilder.board(board: BoardViewState) = child(Board::class) { + attrs.boardViewState = board } - - diff --git a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/pattern.kt b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/pattern.kt index fb64cb5e2..97dbbc16b 100644 --- a/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/pattern.kt +++ b/game-of-life-multiplatform/game-of-life-js/src/main/kotlin/com/novoda/gol/components/pattern.kt @@ -43,4 +43,4 @@ fun RBuilder.pattern(patternEntity: PatternEntity, onPatternSelected: () -> Unit } } } -} \ No newline at end of file +} diff --git a/game-of-life-multiplatform/game-of-life-jvm/src/main/kotlin/com/novoda/gol/Logger.kt b/game-of-life-multiplatform/game-of-life-jvm/src/main/kotlin/com/novoda/gol/Logger.kt new file mode 100644 index 000000000..18620a6ea --- /dev/null +++ b/game-of-life-multiplatform/game-of-life-jvm/src/main/kotlin/com/novoda/gol/Logger.kt @@ -0,0 +1,9 @@ +package com.novoda.gol + +actual object Logger { + + actual fun log(o: Any?) { + System.out.println(o) + } + +}