Fix help callout anchoring and Push remote setup
- Anchor startup help callouts inside the scrollable pane workspace so they scroll with the exercise and terminal content they point at. - Rebuild the Push level remote as a bare sibling repo with a temporary worktree for the remote-only commit, avoiding Android non-bare remote push failures. - Leave a visible local worktree edit in Push while enabling rebase autostash so status, diff, and pull --rebase all behave as expected. - Add native Push setup coverage for diverged status output, plain diff output, and clean pull --rebase behavior.
This commit is contained in:
@@ -53,8 +53,11 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.layout.boundsInRoot
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun GitHugApp() {
|
||||
@@ -95,6 +98,8 @@ fun GitHugApp() {
|
||||
var showHelpOverlay by remember { mutableStateOf(false) }
|
||||
var showHelpOnStart by remember { mutableStateOf(true) }
|
||||
var helpPreferenceInitialized by remember { mutableStateOf(false) }
|
||||
var workspaceBounds by remember { mutableStateOf<Rect?>(null) }
|
||||
var exerciseBounds by remember { mutableStateOf<Rect?>(null) }
|
||||
var promptBounds by remember { mutableStateOf<Rect?>(null) }
|
||||
val currentLevel = levels[currentLevelIndex]
|
||||
|
||||
@@ -495,104 +500,112 @@ fun GitHugApp() {
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
FixedHeader()
|
||||
PaneWorkspace(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
paneLayout = paneLayout,
|
||||
onMovePane = { paneId, delta ->
|
||||
updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
|
||||
},
|
||||
onTogglePane = { paneId ->
|
||||
updatePaneLayout(transform = { layout ->
|
||||
val collapsed = layout.collapsed.toMutableSet()
|
||||
if (!collapsed.add(paneId)) collapsed.remove(paneId)
|
||||
layout.copy(collapsed = collapsed)
|
||||
})
|
||||
},
|
||||
levelsContent = {
|
||||
LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) }
|
||||
},
|
||||
visualContent = { VisualPane(repo) },
|
||||
exerciseContent = {
|
||||
ExercisePane(
|
||||
level = currentLevel,
|
||||
visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null,
|
||||
suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
|
||||
showDescriptionHint = false,
|
||||
onToggleSuggestions = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS
|
||||
if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) {
|
||||
visibleHint = null
|
||||
}
|
||||
applyRecommendedPaneWeights(persist = false)
|
||||
},
|
||||
onHint = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
|
||||
visibleHint = hint
|
||||
activeExerciseDetail = ExerciseDetailPanel.HINT
|
||||
hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
|
||||
applyRecommendedPaneWeights(persist = false)
|
||||
},
|
||||
onReset = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
resetCurrentLevel()
|
||||
},
|
||||
)
|
||||
},
|
||||
terminalContent = {
|
||||
TerminalPane(
|
||||
output = output,
|
||||
inputFieldVersion = inputFieldVersion,
|
||||
commandInput = commandInput,
|
||||
showInputHint = false,
|
||||
onInputHintDismiss = {
|
||||
showTerminalInputHint = false
|
||||
showHelpOverlay = false
|
||||
},
|
||||
onValueChange = {
|
||||
val blockedEcho = suppressedImeEcho
|
||||
if (blockedEcho != null && commandInput.text.isEmpty()) {
|
||||
val blockedTrimmed = blockedEcho.trim()
|
||||
if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) {
|
||||
return@TerminalPane
|
||||
}
|
||||
}
|
||||
suppressedImeEcho = null
|
||||
if (it.text.isNotEmpty()) {
|
||||
showTerminalInputHint = false
|
||||
.fillMaxWidth()
|
||||
.onGloballyPositioned { workspaceBounds = it.boundsInRoot() },
|
||||
) {
|
||||
PaneWorkspace(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
paneLayout = paneLayout,
|
||||
onMovePane = { paneId, delta ->
|
||||
updatePaneLayout(transform = { layout -> movePane(layout, paneId, delta) })
|
||||
},
|
||||
onTogglePane = { paneId ->
|
||||
updatePaneLayout(transform = { layout ->
|
||||
val collapsed = layout.collapsed.toMutableSet()
|
||||
if (!collapsed.add(paneId)) collapsed.remove(paneId)
|
||||
layout.copy(collapsed = collapsed)
|
||||
})
|
||||
},
|
||||
levelsContent = {
|
||||
LevelsPane(levels, currentLevelIndex, completedLevels) { index -> loadLevel(index) }
|
||||
},
|
||||
visualContent = { VisualPane(repo) },
|
||||
exerciseContent = {
|
||||
ExercisePane(
|
||||
level = currentLevel,
|
||||
visibleHint = if (activeExerciseDetail == ExerciseDetailPanel.HINT) visibleHint else null,
|
||||
suggestionsExpanded = activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS,
|
||||
showDescriptionHint = false,
|
||||
onBoundsChanged = { exerciseBounds = it },
|
||||
onToggleSuggestions = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
}
|
||||
commandInput = it
|
||||
activeExerciseDetail = if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) null else ExerciseDetailPanel.SUGGESTIONS
|
||||
if (activeExerciseDetail == ExerciseDetailPanel.SUGGESTIONS) {
|
||||
visibleHint = null
|
||||
}
|
||||
applyRecommendedPaneWeights(persist = false)
|
||||
},
|
||||
onHint = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
val hint = currentLevel.hints.getOrNull(hintIndex) ?: "No more hints for this level."
|
||||
visibleHint = hint
|
||||
activeExerciseDetail = ExerciseDetailPanel.HINT
|
||||
hintIndex = (hintIndex + 1).coerceAtMost(currentLevel.hints.size)
|
||||
applyRecommendedPaneWeights(persist = false)
|
||||
},
|
||||
onReset = {
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
resetCurrentLevel()
|
||||
},
|
||||
)
|
||||
},
|
||||
terminalContent = {
|
||||
TerminalPane(
|
||||
output = output,
|
||||
inputFieldVersion = inputFieldVersion,
|
||||
commandInput = commandInput,
|
||||
showInputHint = false,
|
||||
onInputHintDismiss = {
|
||||
showTerminalInputHint = false
|
||||
showHelpOverlay = false
|
||||
},
|
||||
onValueChange = {
|
||||
val blockedEcho = suppressedImeEcho
|
||||
if (blockedEcho != null && commandInput.text.isEmpty()) {
|
||||
val blockedTrimmed = blockedEcho.trim()
|
||||
if (it.text == blockedEcho || (blockedTrimmed.isNotEmpty() && it.text == blockedTrimmed)) {
|
||||
return@TerminalPane
|
||||
}
|
||||
}
|
||||
suppressedImeEcho = null
|
||||
if (it.text.isNotEmpty()) {
|
||||
showTerminalInputHint = false
|
||||
showExerciseDescriptionHint = false
|
||||
showHelpOverlay = false
|
||||
}
|
||||
commandInput = it
|
||||
},
|
||||
onRun = { runCommand() },
|
||||
onTab = { tabComplete() },
|
||||
onHelp = { showCommandHelp() },
|
||||
onCursorLeft = { moveCursor(-1) },
|
||||
onCursorRight = { moveCursor(1) },
|
||||
onHistoryUp = { historyUp() },
|
||||
onHistoryDown = { historyDown() },
|
||||
onPromptBoundsChanged = { promptBounds = it },
|
||||
)
|
||||
},
|
||||
)
|
||||
if (showHelpOverlay) {
|
||||
HelpCalloutOverlay(
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = { showHelpOnStart = it },
|
||||
onOk = {
|
||||
showHelpOverlay = false
|
||||
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
||||
},
|
||||
onRun = { runCommand() },
|
||||
onTab = { tabComplete() },
|
||||
onHelp = { showCommandHelp() },
|
||||
onCursorLeft = { moveCursor(-1) },
|
||||
onCursorRight = { moveCursor(1) },
|
||||
onHistoryUp = { historyUp() },
|
||||
onHistoryDown = { historyDown() },
|
||||
onPromptBoundsChanged = { promptBounds = it },
|
||||
onClose = { showHelpOverlay = false },
|
||||
workspaceBounds = workspaceBounds,
|
||||
exerciseBounds = exerciseBounds,
|
||||
promptBounds = promptBounds,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
if (showHelpOverlay) {
|
||||
HelpCalloutOverlay(
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = { showHelpOnStart = it },
|
||||
onOk = {
|
||||
showHelpOverlay = false
|
||||
scope.launch { gameProgressStore.saveShowHelpOnStart(showHelpOnStart) }
|
||||
},
|
||||
onClose = { showHelpOverlay = false },
|
||||
promptBounds = promptBounds,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,25 +683,41 @@ private fun HelpCalloutOverlay(
|
||||
onShowHelpOnStartChange: (Boolean) -> Unit,
|
||||
onOk: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
workspaceBounds: Rect?,
|
||||
exerciseBounds: Rect?,
|
||||
promptBounds: Rect?,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val workspaceTop = workspaceBounds?.top ?: 0f
|
||||
val insetPx = with(density) { 14.dp.roundToPx() }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
HelpBubble(
|
||||
text = "Read the exercise description, then solve it by entering commands below.",
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
modifier = exerciseBounds?.let { bounds ->
|
||||
Modifier.offset {
|
||||
IntOffset(
|
||||
x = insetPx,
|
||||
y = (bounds.top - workspaceTop - insetPx).roundToInt().coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
} ?: Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
)
|
||||
HelpBubbleWithControls(
|
||||
modifier = promptBounds?.let { bounds ->
|
||||
val calloutTop = with(density) { (bounds.top.toDp() - 148.dp).coerceAtLeast(12.dp) }
|
||||
Modifier.offset { IntOffset(0, with(density) { calloutTop.roundToPx() }) }
|
||||
val bubbleHeight = with(density) { 190.dp.roundToPx() }
|
||||
Modifier.offset {
|
||||
IntOffset(
|
||||
x = insetPx,
|
||||
y = (bounds.top - workspaceTop - bubbleHeight).roundToInt().coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
} ?: Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(bottom = 96.dp),
|
||||
.padding(start = 14.dp, bottom = 96.dp),
|
||||
text = "Tap the prompt to enter a command.",
|
||||
showHelpOnStart = showHelpOnStart,
|
||||
onShowHelpOnStartChange = onShowHelpOnStartChange,
|
||||
|
||||
Reference in New Issue
Block a user