Make manual level selection responsive

This commit is contained in:
Joe Tretter
2026-06-25 10:02:06 -05:00
parent 82e38de3bb
commit 035851ac05
3 changed files with 57 additions and 7 deletions

View File

@@ -20,8 +20,8 @@ android {
applicationId = "solutions.tretter.githugandroid" applicationId = "solutions.tretter.githugandroid"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 175 versionCode = 176
versionName = "0.1.174" versionName = "0.1.175"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performImeAction import androidx.compose.ui.test.performImeAction
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
@@ -62,6 +63,21 @@ class GitRepositoryRuntimeInstrumentedTest {
} }
} }
@Test
fun selectingLevelFromLevelsPaneUpdatesExerciseImmediately() {
launchFreshApp().use {
composeRule.onNodeWithTag("level-submodule")
.performScrollTo()
.performClick()
composeRule.waitUntil(timeoutMillis = 1_500) {
composeRule.onAllNodes(hasText(submoduleLevel().title, substring = true))
.fetchSemanticsNodes()
.isNotEmpty()
}
}
}
private fun launchFreshApp(): ActivityScenario<MainActivity> { private fun launchFreshApp(): ActivityScenario<MainActivity> {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
File(context.filesDir, "githug-sandboxes").deleteRecursively() File(context.filesDir, "githug-sandboxes").deleteRecursively()

View File

@@ -34,8 +34,10 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun GitHugApp() { fun GitHugApp() {
@@ -73,6 +75,8 @@ fun GitHugApp() {
var historyIndex by remember { mutableStateOf(-1) } var historyIndex by remember { mutableStateOf(-1) }
var historyDraft by remember { mutableStateOf("") } var historyDraft by remember { mutableStateOf("") }
var hasRestoredProgress by remember { mutableStateOf(false) } var hasRestoredProgress by remember { mutableStateOf(false) }
var isPreparingLevel by remember { mutableStateOf(false) }
var levelLoadRequestId by remember { mutableStateOf(0) }
var editorState by remember { mutableStateOf<TextEditorState?>(null) } var editorState by remember { mutableStateOf<TextEditorState?>(null) }
var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) } var gitMessageEditorState by remember { mutableStateOf<GitMessageEditorState?>(null) }
var manPageState by remember { mutableStateOf<ManPageState?>(null) } var manPageState by remember { mutableStateOf<ManPageState?>(null) }
@@ -233,11 +237,14 @@ fun GitHugApp() {
index: Int, index: Int,
message: List<String> = listOf("Loaded level: ${levels[index].title}"), message: List<String> = listOf("Loaded level: ${levels[index].title}"),
keepSuppressedImeEcho: Boolean = false, keepSuppressedImeEcho: Boolean = false,
prepareInBackground: Boolean = false,
) { ) {
AppLog.d("GitHugApp", "Loading level index=$index id=${levels[index].id} title=${levels[index].title}") val level = levels[index]
val requestId = levelLoadRequestId + 1
levelLoadRequestId = requestId
AppLog.d("GitHugApp", "Loading level index=$index id=${level.id} title=${level.title} background=$prepareInBackground")
currentLevelIndex = index currentLevelIndex = index
repo = runtime.prepareLevel(levels[index]) output = if (prepareInBackground) listOf("Preparing level: ${level.title}") else message
output = message
clearCommandInput(recreateField = true) clearCommandInput(recreateField = true)
if (!keepSuppressedImeEcho) { if (!keepSuppressedImeEcho) {
suppressedImeEcho = null suppressedImeEcho = null
@@ -250,10 +257,29 @@ fun GitHugApp() {
editorState = null editorState = null
gitMessageEditorState = null gitMessageEditorState = null
manPageState = null manPageState = null
if (prepareInBackground) {
isPreparingLevel = true
repo = RepoState()
scope.launch {
val preparedRepo = withContext(Dispatchers.Default) {
runtime.prepareLevel(level)
}
if (levelLoadRequestId == requestId) {
AppLog.d("GitHugApp", "Prepared level index=$index id=${level.id}")
repo = preparedRepo
output = message
isPreparingLevel = false
clearCommandInput(recreateField = true)
}
}
} else {
isPreparingLevel = false
repo = runtime.prepareLevel(level)
}
paneLayout = paneLayout.copy( paneLayout = paneLayout.copy(
weights = paneLayout.weights + recommendedPaneWeights( weights = paneLayout.weights + recommendedPaneWeights(
heights = recommendedPaneHeights( heights = recommendedPaneHeights(
level = levels[index], level = level,
levelCount = levels.size, levelCount = levels.size,
outputLineCount = 1, outputLineCount = 1,
screenHeightDp = screenHeightDp, screenHeightDp = screenHeightDp,
@@ -457,6 +483,12 @@ fun GitHugApp() {
val submittedText = commandInput.text val submittedText = commandInput.text
val raw = submittedText.trim() val raw = submittedText.trim()
if (raw.isBlank()) return if (raw.isBlank()) return
if (isPreparingLevel) {
output = output + "Still preparing ${currentLevel.title}. Try again in a moment."
clearCommandInput(recreateField = true)
applyRecommendedPaneWeights(persist = false, outputLineCount = terminalOutputLineCount(output))
return
}
val isInteractiveInput = repo.interactiveAddSession != null val isInteractiveInput = repo.interactiveAddSession != null
showHelpOverlay = false showHelpOverlay = false
@@ -560,7 +592,9 @@ fun GitHugApp() {
}) })
}, },
levelsContent = { levelsContent = {
LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) } LevelsPane(levels, currentLevelIndex, completedLevels) { index ->
loadLevel(index, prepareInBackground = true)
}
}, },
visualContent = { VisualPane(repo) }, visualContent = { VisualPane(repo) },
exerciseContent = { exerciseContent = {