Skip to content

Commit e4b8cdb

Browse files
authored
Merge pull request #20 from joreilly/junie
More Junie UI/game logic updates
2 parents 9a68905 + 33e1605 commit e4b8cdb

File tree

4 files changed

+97
-53
lines changed

4 files changed

+97
-53
lines changed

androidApp/src/main/java/dev/johnoreilly/wordmaster/androidApp/MainActivity.kt

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.foundation.text.KeyboardActions
3737
import androidx.compose.ui.unit.dp
3838
import androidx.compose.ui.unit.sp
3939
import androidx.compose.ui.input.key.onKeyEvent
40+
import androidx.compose.ui.input.key.onPreviewKeyEvent
4041
import androidx.compose.ui.input.key.Key
4142
import androidx.compose.ui.input.key.KeyEventType
4243
import androidx.compose.ui.input.key.key
@@ -85,7 +86,10 @@ fun WordMasterView(padding: Modifier) {
8586
val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState()
8687

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

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

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

101-
var modifier = Modifier.width(55.dp).height(55.dp)
102-
if (guessAttempt == 0 && character == 0) {
103-
modifier = modifier.focusRequester(focusRequester)
104-
}
105+
var modifier = Modifier.width(55.dp).height(55.dp).focusRequester(cellRequesters[guessAttempt][character])
105106

106107
TextField(
107108
value = boardGuesses[guessAttempt][character],
108-
onValueChange = {
109-
if (it.length <= 1 && guessAttempt == wordMasterService.currentGuessAttempt) {
110-
wordMasterService.setGuess(
111-
guessAttempt,
112-
character,
113-
it.uppercase()
114-
)
115-
if (it.isNotEmpty() && character < WordMasterService.NUMBER_LETTERS - 1) {
116-
// Only move within the same row; don't advance to the next row until the guess is submitted
117-
focusManager.moveFocus(FocusDirection.Next)
109+
onValueChange = { newValue ->
110+
if (guessAttempt == wordMasterService.currentGuessAttempt) {
111+
val upper = newValue.uppercase()
112+
val capped = if (upper.length > 1) upper.substring(0, 1) else upper
113+
val previous = boardGuesses[guessAttempt][character]
114+
115+
if (capped != previous) {
116+
wordMasterService.setGuess(
117+
guessAttempt,
118+
character,
119+
capped
120+
)
121+
}
122+
123+
if (capped.isNotEmpty()) {
124+
if (character < WordMasterService.NUMBER_LETTERS - 1) {
125+
// Advance to next column in the same row
126+
focusManager.moveFocus(FocusDirection.Next)
127+
}
128+
} else {
129+
// If we deleted the last character in this cell, move back to previous cell in same row
130+
if (previous.isNotEmpty() && character > 0) {
131+
cellRequesters[guessAttempt][character - 1].requestFocus()
132+
}
118133
}
119134
}
120135
},
121-
modifier = modifier.onKeyEvent {
122-
if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.NumPadEnter)) {
123-
if (guessAttempt == wordMasterService.currentGuessAttempt) {
124-
var filled = true
125-
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
126-
if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break }
136+
modifier = modifier
137+
.onPreviewKeyEvent {
138+
if (guessAttempt == wordMasterService.currentGuessAttempt && (it.key == Key.Backspace || it.key == Key.Delete) && it.type == KeyEventType.KeyDown) {
139+
val currentVal = boardGuesses[guessAttempt][character]
140+
if (currentVal.isEmpty() && character > 0) {
141+
cellRequesters[guessAttempt][character - 1].requestFocus()
142+
return@onPreviewKeyEvent true
127143
}
128-
if (filled) {
129-
wordMasterService.checkGuess()
130-
// After submitting a guess, move focus to the next row's first cell
131-
focusManager.moveFocus(FocusDirection.Next)
132-
return@onKeyEvent true
144+
}
145+
false
146+
}
147+
.onKeyEvent {
148+
if (it.type == KeyEventType.KeyUp && it.key == Key.Backspace) {
149+
if (guessAttempt == wordMasterService.currentGuessAttempt) {
150+
val currentVal = boardGuesses[guessAttempt][character]
151+
if (currentVal.isEmpty() && character > 0) {
152+
cellRequesters[guessAttempt][character - 1].requestFocus()
153+
return@onKeyEvent true
154+
}
155+
}
156+
} else if (it.type == KeyEventType.KeyUp && (it.key == Key.Enter || it.key == Key.NumPadEnter)) {
157+
if (guessAttempt == wordMasterService.currentGuessAttempt) {
158+
var filled = true
159+
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
160+
if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break }
161+
}
162+
if (filled) {
163+
wordMasterService.checkGuess()
164+
// After submitting a guess, move focus to the next row's first cell
165+
focusManager.moveFocus(FocusDirection.Next)
166+
return@onKeyEvent true
167+
}
133168
}
134169
}
170+
false
135171
}
136-
false
137-
}
138172
.border(1.dp, Color.Black.copy(alpha = 0.6f), androidx.compose.foundation.shape.RoundedCornerShape(10.dp)),
139173
singleLine = true,
140174
keyboardOptions = KeyboardOptions(
@@ -173,9 +207,11 @@ fun WordMasterView(padding: Modifier) {
173207
),
174208
)
175209

176-
DisposableEffect(Unit) {
177-
focusRequester.requestFocus()
178-
onDispose { }
210+
if (guessAttempt == 0 && character == 0) {
211+
DisposableEffect(Unit) {
212+
cellRequesters[0][0].requestFocus()
213+
onDispose { }
214+
}
179215
}
180216
}
181217
}
@@ -211,7 +247,7 @@ fun WordMasterView(padding: Modifier) {
211247
Spacer(Modifier.width(16.dp))
212248
Button(onClick = {
213249
wordMasterService.resetGame()
214-
focusRequester.requestFocus()
250+
cellRequesters[0][0].requestFocus()
215251
}) {
216252
Text("New Game")
217253
}
@@ -226,7 +262,7 @@ fun WordMasterView(padding: Modifier) {
226262
Button(onClick = {
227263
wordMasterService.resetGame()
228264
// Re-focus first cell after reset
229-
focusRequester.requestFocus()
265+
cellRequesters[0][0].requestFocus()
230266
}) {
231267
Text("OK")
232268
}

compose-desktop/src/main/kotlin/main.kt

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,14 @@ fun WordMasterView() {
4646
val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState()
4747

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

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

@@ -73,13 +72,10 @@ fun WordMasterView() {
7372
// Move focus explicitly to next row’s first cell
7473
val nextRow = current + 1
7574
if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) {
76-
rowFirstCellRequesters[nextRow].requestFocus()
75+
cellRequesters[nextRow][0].requestFocus()
7776
}
7877
true
7978
} else false
80-
} else if (it.key == Key.Backspace) {
81-
focusManager.moveFocus(FocusDirection.Previous)
82-
true
8379
} else {
8480
false
8581
}
@@ -97,9 +93,7 @@ fun WordMasterView() {
9793
.padding(2.dp)
9894
.width(64.dp)
9995
.height(64.dp)
100-
if (character == 0) {
101-
modifier = modifier.focusRequester(rowFirstCellRequesters[guessAttempt])
102-
}
96+
modifier = modifier.focusRequester(cellRequesters[guessAttempt][character])
10397

10498
TextField(
10599
enabled = guessAttempt == wordMasterService.currentGuessAttempt,
@@ -121,7 +115,15 @@ fun WordMasterView() {
121115
}
122116
}
123117
},
124-
modifier = modifier.border(1.dp, Black.copy(alpha = 0.6f), RoundedCornerShape(10.dp)),
118+
modifier = modifier.border(1.dp, Black.copy(alpha = 0.6f), RoundedCornerShape(10.dp)).onKeyEvent {
119+
if (it.key == Key.Backspace && guessAttempt == wordMasterService.currentGuessAttempt) {
120+
val currentVal = boardGuesses[guessAttempt][character]
121+
if (currentVal.isEmpty() && character > 0) {
122+
cellRequesters[guessAttempt][character - 1].requestFocus()
123+
true
124+
} else false
125+
} else false
126+
},
125127
singleLine = true,
126128
textStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center),
127129
colors = TextFieldDefaults.textFieldColors(
@@ -136,7 +138,7 @@ fun WordMasterView() {
136138

137139
if (guessAttempt == 0 && character == 0) {
138140
DisposableEffect(Unit) {
139-
rowFirstCellRequesters[0].requestFocus()
141+
cellRequesters[0][0].requestFocus()
140142
onDispose { }
141143
}
142144
}
@@ -165,7 +167,7 @@ fun WordMasterView() {
165167
// Move focus explicitly to next row’s first cell
166168
val nextRow = current + 1
167169
if (nextRow < WordMasterService.MAX_NUMBER_OF_GUESSES) {
168-
rowFirstCellRequesters[nextRow].requestFocus()
170+
cellRequesters[nextRow][0].requestFocus()
169171
}
170172
}
171173
}) {
@@ -174,7 +176,7 @@ fun WordMasterView() {
174176
Spacer(Modifier.width(16.dp))
175177
Button(onClick = {
176178
wordMasterService.resetGame()
177-
rowFirstCellRequesters[0].requestFocus()
179+
cellRequesters[0][0].requestFocus()
178180
}) {
179181
Text("New Game")
180182
}
@@ -188,7 +190,7 @@ fun WordMasterView() {
188190
confirmButton = {
189191
Button(onClick = {
190192
wordMasterService.resetGame()
191-
rowFirstCellRequesters[0].requestFocus()
193+
cellRequesters[0][0].requestFocus()
192194
}) {
193195
Text("OK")
194196
}

iosApp/iosApp/ContentView.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ struct ContentView: View {
2323
let upper = newValue.uppercased()
2424
let capped = String(upper.prefix(1))
2525

26-
if capped != viewModel.getGuess(guessAttempt: guessNumber, character: character) {
26+
let previous = viewModel.getGuess(guessAttempt: guessNumber, character: character)
27+
if capped != previous {
2728
viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: capped)
2829

2930
// Move focus to the next cell when a single character is entered
@@ -34,8 +35,13 @@ struct ContentView: View {
3435
DispatchQueue.main.async {
3536
focusedPos = FocusPos(row: guessNumber, col: nextCol)
3637
}
37-
} else {
38-
// Optionally keep focus or move to next row's first cell; we'll keep it here
38+
}
39+
} else {
40+
// If we deleted the last character in this cell, move back to previous cell in same row
41+
if !previous.isEmpty && character > 0 {
42+
DispatchQueue.main.async {
43+
focusedPos = FocusPos(row: guessNumber, col: character - 1)
44+
}
3945
}
4046
}
4147
}

iosApp/iosApp/ViewModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ViewModel: ObservableObject {
4343
do {
4444
let stream = asyncSequence(for: wordMasterService.revealedAnswer)
4545
for try await data in stream {
46-
self.revealedAnswer = data as? String
46+
self.revealedAnswer = data
4747
}
4848
} catch {
4949
print("Failed with error: \(error)")

0 commit comments

Comments
 (0)