Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
Expand Down Expand Up @@ -85,7 +86,10 @@ fun WordMasterView(padding: Modifier) {
val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState()

val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
// FocusRequesters for every cell to enable precise intra-row navigation (e.g., Backspace behavior)
val cellRequesters = remember {
List(WordMasterService.MAX_NUMBER_OF_GUESSES) { List(WordMasterService.NUMBER_LETTERS) { FocusRequester() } }
}

Row(padding.fillMaxSize().padding(16.dp), horizontalArrangement = Center, verticalAlignment = Alignment.CenterVertically) {

Expand All @@ -98,43 +102,73 @@ fun WordMasterView(padding: Modifier) {
horizontalAlignment = Alignment.CenterHorizontally
) {

var modifier = Modifier.width(55.dp).height(55.dp)
if (guessAttempt == 0 && character == 0) {
modifier = modifier.focusRequester(focusRequester)
}
var modifier = Modifier.width(55.dp).height(55.dp).focusRequester(cellRequesters[guessAttempt][character])

TextField(
value = boardGuesses[guessAttempt][character],
onValueChange = {
if (it.length <= 1 && guessAttempt == wordMasterService.currentGuessAttempt) {
wordMasterService.setGuess(
guessAttempt,
character,
it.uppercase()
)
if (it.isNotEmpty() && character < WordMasterService.NUMBER_LETTERS - 1) {
// Only move within the same row; don't advance to the next row until the guess is submitted
focusManager.moveFocus(FocusDirection.Next)
onValueChange = { newValue ->
if (guessAttempt == wordMasterService.currentGuessAttempt) {
val upper = newValue.uppercase()
val capped = if (upper.length > 1) upper.substring(0, 1) else upper
val previous = boardGuesses[guessAttempt][character]

if (capped != previous) {
wordMasterService.setGuess(
guessAttempt,
character,
capped
)
}

if (capped.isNotEmpty()) {
if (character < WordMasterService.NUMBER_LETTERS - 1) {
// Advance to next column in the same row
focusManager.moveFocus(FocusDirection.Next)
}
} else {
// If we deleted the last character in this cell, move back to previous cell in same row
if (previous.isNotEmpty() && character > 0) {
cellRequesters[guessAttempt][character - 1].requestFocus()
}
}
}
},
modifier = modifier.onKeyEvent {
if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.NumPadEnter)) {
if (guessAttempt == wordMasterService.currentGuessAttempt) {
var filled = true
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break }
modifier = modifier
.onPreviewKeyEvent {
if (guessAttempt == wordMasterService.currentGuessAttempt && (it.key == Key.Backspace || it.key == Key.Delete) && it.type == KeyEventType.KeyDown) {
val currentVal = boardGuesses[guessAttempt][character]
if (currentVal.isEmpty() && character > 0) {
cellRequesters[guessAttempt][character - 1].requestFocus()
return@onPreviewKeyEvent true
}
if (filled) {
wordMasterService.checkGuess()
// After submitting a guess, move focus to the next row's first cell
focusManager.moveFocus(FocusDirection.Next)
return@onKeyEvent true
}
false
}
.onKeyEvent {
if (it.type == KeyEventType.KeyUp && it.key == Key.Backspace) {
if (guessAttempt == wordMasterService.currentGuessAttempt) {
val currentVal = boardGuesses[guessAttempt][character]
if (currentVal.isEmpty() && character > 0) {
cellRequesters[guessAttempt][character - 1].requestFocus()
return@onKeyEvent true
}
}
} else if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.NumPadEnter)) {
if (guessAttempt == wordMasterService.currentGuessAttempt) {
var filled = true
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break }
}
if (filled) {
wordMasterService.checkGuess()
// After submitting a guess, move focus to the next row's first cell
focusManager.moveFocus(FocusDirection.Next)
return@onKeyEvent true
}
}
}
false
}
false
}
.border(1.dp, Color.Black.copy(alpha = 0.6f), androidx.compose.foundation.shape.RoundedCornerShape(10.dp)),
singleLine = true,
keyboardOptions = KeyboardOptions(
Expand Down Expand Up @@ -173,9 +207,11 @@ fun WordMasterView(padding: Modifier) {
),
)

DisposableEffect(Unit) {
focusRequester.requestFocus()
onDispose { }
if (guessAttempt == 0 && character == 0) {
DisposableEffect(Unit) {
cellRequesters[0][0].requestFocus()
onDispose { }
}
}
}
}
Expand Down Expand Up @@ -211,7 +247,7 @@ fun WordMasterView(padding: Modifier) {
Spacer(Modifier.width(16.dp))
Button(onClick = {
wordMasterService.resetGame()
focusRequester.requestFocus()
cellRequesters[0][0].requestFocus()
}) {
Text("New Game")
}
Expand All @@ -226,7 +262,7 @@ fun WordMasterView(padding: Modifier) {
Button(onClick = {
wordMasterService.resetGame()
// Re-focus first cell after reset
focusRequester.requestFocus()
cellRequesters[0][0].requestFocus()
}) {
Text("OK")
}
Expand Down
34 changes: 18 additions & 16 deletions compose-desktop/src/main/kotlin/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ fun WordMasterView() {
val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState()

val focusManager = LocalFocusManager.current
// FocusRequesters for the first cell of each row to allow precise focusing after submission
val rowFirstCellRequesters = remember { List(WordMasterService.MAX_NUMBER_OF_GUESSES) { FocusRequester() } }
// FocusRequesters for every cell to precisely control focus navigation within rows
val cellRequesters = remember { List(WordMasterService.MAX_NUMBER_OF_GUESSES) { List(WordMasterService.NUMBER_LETTERS) { FocusRequester() } } }

// Ensure focus shifts to the first cell of the current row after a guess submission/recomposition
val currentAttempt = wordMasterService.currentGuessAttempt
LaunchedEffect(currentAttempt) {
if (currentAttempt in 0 until WordMasterService.MAX_NUMBER_OF_GUESSES) {
// Post-recomposition, request focus on the first cell of the active row
rowFirstCellRequesters[currentAttempt].requestFocus()
cellRequesters[currentAttempt][0].requestFocus()
}
}

Expand All @@ -73,13 +72,10 @@ fun WordMasterView() {
// Move focus explicitly to next row’s first cell
val nextRow = current + 1
if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) {
rowFirstCellRequesters[nextRow].requestFocus()
cellRequesters[nextRow][0].requestFocus()
}
true
} else false
} else if (it.key == Key.Backspace) {
focusManager.moveFocus(FocusDirection.Previous)
true
} else {
false
}
Expand All @@ -97,9 +93,7 @@ fun WordMasterView() {
.padding(2.dp)
.width(64.dp)
.height(64.dp)
if (character == 0) {
modifier = modifier.focusRequester(rowFirstCellRequesters[guessAttempt])
}
modifier = modifier.focusRequester(cellRequesters[guessAttempt][character])

TextField(
enabled = guessAttempt == wordMasterService.currentGuessAttempt,
Expand All @@ -121,7 +115,15 @@ fun WordMasterView() {
}
}
},
modifier = modifier.border(1.dp, Black.copy(alpha = 0.6f), RoundedCornerShape(10.dp)),
modifier = modifier.border(1.dp, Black.copy(alpha = 0.6f), RoundedCornerShape(10.dp)).onKeyEvent {
if (it.key == Key.Backspace && guessAttempt == wordMasterService.currentGuessAttempt) {
val currentVal = boardGuesses[guessAttempt][character]
if (currentVal.isEmpty() && character > 0) {
cellRequesters[guessAttempt][character - 1].requestFocus()
true
} else false
} else false
},
singleLine = true,
textStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center),
colors = TextFieldDefaults.textFieldColors(
Expand All @@ -136,7 +138,7 @@ fun WordMasterView() {

if (guessAttempt == 0 && character == 0) {
DisposableEffect(Unit) {
rowFirstCellRequesters[0].requestFocus()
cellRequesters[0][0].requestFocus()
onDispose { }
}
}
Expand Down Expand Up @@ -165,7 +167,7 @@ fun WordMasterView() {
// Move focus explicitly to next row’s first cell
val nextRow = current + 1
if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) {
rowFirstCellRequesters[nextRow].requestFocus()
cellRequesters[nextRow][0].requestFocus()
}
}
}) {
Expand All @@ -174,7 +176,7 @@ fun WordMasterView() {
Spacer(Modifier.width(16.dp))
Button(onClick = {
wordMasterService.resetGame()
rowFirstCellRequesters[0].requestFocus()
cellRequesters[0][0].requestFocus()
}) {
Text("New Game")
}
Expand All @@ -188,7 +190,7 @@ fun WordMasterView() {
confirmButton = {
Button(onClick = {
wordMasterService.resetGame()
rowFirstCellRequesters[0].requestFocus()
cellRequesters[0][0].requestFocus()
}) {
Text("OK")
}
Expand Down
12 changes: 9 additions & 3 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ struct ContentView: View {
let upper = newValue.uppercased()
let capped = String(upper.prefix(1))

if capped != viewModel.getGuess(guessAttempt: guessNumber, character: character) {
let previous = viewModel.getGuess(guessAttempt: guessNumber, character: character)
if capped != previous {
viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: capped)

// Move focus to the next cell when a single character is entered
Expand All @@ -34,8 +35,13 @@ struct ContentView: View {
DispatchQueue.main.async {
focusedPos = FocusPos(row: guessNumber, col: nextCol)
}
} else {
// Optionally keep focus or move to next row's first cell; we'll keep it here
}
} else {
// If we deleted the last character in this cell, move back to previous cell in same row
if !previous.isEmpty && character > 0 {
DispatchQueue.main.async {
focusedPos = FocusPos(row: guessNumber, col: character - 1)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion iosApp/iosApp/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ViewModel: ObservableObject {
do {
let stream = asyncSequence(for: wordMasterService.revealedAnswer)
for try await data in stream {
self.revealedAnswer = data as? String
self.revealedAnswer = data
}
} catch {
print("Failed with error: \(error)")
Expand Down
Loading