Skip to content

Commit 971aa3d

Browse files
committed
Junie UI updates
1 parent f1eaf91 commit 971aa3d

File tree

5 files changed

+257
-30
lines changed

5 files changed

+257
-30
lines changed

androidApp/build.gradle.kts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,6 @@ android {
3131
sourceCompatibility = JavaVersion.VERSION_17
3232
targetCompatibility = JavaVersion.VERSION_17
3333
}
34-
35-
// kotlinOptions {
36-
// jvmTarget = "17"
37-
// }
3834
}
3935

4036

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

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.material3.Scaffold
1717
import androidx.compose.material3.Text
1818
import androidx.compose.material3.TextField
1919
import androidx.compose.material3.TextFieldDefaults
20+
import androidx.compose.material3.MaterialTheme
2021
import androidx.compose.material3.TopAppBar
2122
import androidx.compose.runtime.*
2223
import androidx.compose.ui.Alignment
@@ -29,8 +30,17 @@ import androidx.compose.ui.platform.LocalContext
2930
import androidx.compose.ui.platform.LocalFocusManager
3031
import androidx.compose.ui.text.TextStyle
3132
import androidx.compose.ui.text.style.TextAlign
33+
import androidx.compose.ui.text.input.KeyboardCapitalization
34+
import androidx.compose.ui.text.input.ImeAction
35+
import androidx.compose.foundation.text.KeyboardOptions
36+
import androidx.compose.foundation.text.KeyboardActions
3237
import androidx.compose.ui.unit.dp
3338
import androidx.compose.ui.unit.sp
39+
import androidx.compose.ui.input.key.onKeyEvent
40+
import androidx.compose.ui.input.key.Key
41+
import androidx.compose.ui.input.key.KeyEventType
42+
import androidx.compose.ui.input.key.key
43+
import androidx.compose.ui.input.key.type
3444
import dev.johnoreilly.wordmaster.shared.LetterStatus
3545
import dev.johnoreilly.wordmaster.shared.WordMasterService
3646
import dev.johnoreilly.wordmaster.androidApp.theme.WordMasterTheme
@@ -71,6 +81,8 @@ fun WordMasterView(padding: Modifier) {
7181

7282
val boardGuesses by wordMasterService.boardGuesses.collectAsState()
7383
val boardStatus by wordMasterService.boardStatus.collectAsState()
84+
val revealedAnswer by wordMasterService.revealedAnswer.collectAsState()
85+
val lastGuessCorrect by wordMasterService.lastGuessCorrect.collectAsState()
7486

7587
val focusManager = LocalFocusManager.current
7688
val focusRequester = remember { FocusRequester() }
@@ -82,7 +94,7 @@ fun WordMasterView(padding: Modifier) {
8294
Row(horizontalArrangement = Arrangement.SpaceBetween) {
8395
for (character in 0 until WordMasterService.NUMBER_LETTERS) {
8496
Column(
85-
Modifier.padding(4.dp).background(Color.White).border(1.dp, Color.Black),
97+
Modifier.padding(4.dp),
8698
horizontalAlignment = Alignment.CenterHorizontally
8799
) {
88100

@@ -100,14 +112,64 @@ fun WordMasterView(padding: Modifier) {
100112
character,
101113
it.uppercase()
102114
)
103-
focusManager.moveFocus(FocusDirection.Next)
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)
118+
}
104119
}
105120
},
106-
modifier = modifier,
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 }
127+
}
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
133+
}
134+
}
135+
}
136+
false
137+
}
138+
.border(1.dp, Color.Black.copy(alpha = 0.6f), androidx.compose.foundation.shape.RoundedCornerShape(10.dp)),
139+
singleLine = true,
140+
keyboardOptions = KeyboardOptions(
141+
capitalization = KeyboardCapitalization.Characters,
142+
imeAction = ImeAction.Done
143+
),
144+
keyboardActions = KeyboardActions(
145+
onDone = {
146+
if (guessAttempt == wordMasterService.currentGuessAttempt) {
147+
var filled = true
148+
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
149+
if (boardGuesses[guessAttempt][c].isEmpty()) { filled = false; break }
150+
}
151+
if (filled) {
152+
wordMasterService.checkGuess()
153+
// After submitting a guess, move focus to the next row's first cell
154+
focusManager.moveFocus(FocusDirection.Next)
155+
}
156+
}
157+
}
158+
),
107159
textStyle = TextStyle(fontSize = 14.sp, textAlign = TextAlign.Center),
160+
shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp),
108161
colors = TextFieldDefaults.colors(
162+
focusedTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]),
109163
unfocusedTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]),
164+
disabledTextColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]),
165+
cursorColor = mapLetterStatusToTextColor(boardStatus[guessAttempt][character]),
166+
focusedContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]),
110167
unfocusedContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]),
168+
disabledContainerColor = mapLetterStatusToBackgroundColor(boardStatus[guessAttempt][character]),
169+
focusedIndicatorColor = Color.Transparent,
170+
unfocusedIndicatorColor = Color.Transparent,
171+
disabledIndicatorColor = Color.Transparent,
172+
errorIndicatorColor = Color.Transparent,
111173
),
112174
)
113175

@@ -121,9 +183,28 @@ fun WordMasterView(padding: Modifier) {
121183
}
122184

123185
Spacer(Modifier.height(16.dp))
186+
187+
if (revealedAnswer != null) {
188+
Text(
189+
text = "Answer: $revealedAnswer",
190+
style = TextStyle(fontSize = 18.sp, color = MaterialTheme.colorScheme.onSurface)
191+
)
192+
Spacer(Modifier.height(12.dp))
193+
}
194+
124195
Row(horizontalArrangement = Arrangement.Center) {
125196
Button(onClick = {
126-
wordMasterService.checkGuess()
197+
// Only submit and advance focus if the current row is filled
198+
val current = wordMasterService.currentGuessAttempt
199+
var filled = true
200+
for (c in 0 until WordMasterService.NUMBER_LETTERS) {
201+
if (boardGuesses[current][c].isEmpty()) { filled = false; break }
202+
}
203+
if (filled) {
204+
wordMasterService.checkGuess()
205+
// Move focus to next row's first cell
206+
focusManager.moveFocus(FocusDirection.Next)
207+
}
127208
}) {
128209
Text("Guess")
129210
}
@@ -135,6 +216,23 @@ fun WordMasterView(padding: Modifier) {
135216
Text("New Game")
136217
}
137218
}
219+
220+
if (lastGuessCorrect) {
221+
androidx.compose.material3.AlertDialog(
222+
onDismissRequest = { /* keep dialog until OK pressed */ },
223+
title = { Text("You win!") },
224+
text = { Text("Great job guessing the word.") },
225+
confirmButton = {
226+
Button(onClick = {
227+
wordMasterService.resetGame()
228+
// Re-focus first cell after reset
229+
focusRequester.requestFocus()
230+
}) {
231+
Text("OK")
232+
}
233+
}
234+
)
235+
}
138236
}
139237
}
140238

iosApp/iosApp/ContentView.swift

Lines changed: 113 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,146 @@ import KMPNativeCoroutinesAsync
44

55
struct ContentView: View {
66
@StateObject private var viewModel = ViewModel()
7+
8+
// Focus handling for per-cell focus movement
9+
private struct FocusPos: Hashable { let row: Int; let col: Int }
10+
@FocusState private var focusedPos: FocusPos?
11+
@State private var showWinAlert: Bool = false
712

813
var body: some View {
914
NavigationView {
10-
VStack {
11-
ForEach(0 ..< viewModel.getMaxNumberGuesses()) { guessNumber in
12-
HStack {
13-
ForEach(0 ..< viewModel.getMaxNumberLetters()) { character in
14-
15+
VStack(spacing: 16) {
16+
ForEach(0 ..< viewModel.getMaxNumberGuesses(), id: \.self) { guessNumber in
17+
HStack(spacing: 8) {
18+
ForEach(0 ..< viewModel.getMaxNumberLetters(), id: \.self) { character in
1519
let guessBinding = Binding<String>(
1620
get: { viewModel.getGuess(guessAttempt: guessNumber, character: character) },
17-
set: {
18-
if ($0.count <= 1) {
19-
viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: $0)
21+
set: { newValue in
22+
// Force uppercase and limit to first character
23+
let upper = newValue.uppercased()
24+
let capped = String(upper.prefix(1))
25+
26+
if capped != viewModel.getGuess(guessAttempt: guessNumber, character: character) {
27+
viewModel.setGuess(guessAttempt: guessNumber, character: character, guess: capped)
28+
29+
// Move focus to the next cell when a single character is entered
30+
if !capped.isEmpty {
31+
let nextCol = character + 1
32+
if nextCol < viewModel.getMaxNumberLetters() {
33+
// Advance to next column in same row
34+
DispatchQueue.main.async {
35+
focusedPos = FocusPos(row: guessNumber, col: nextCol)
36+
}
37+
} else {
38+
// Optionally keep focus or move to next row's first cell; we'll keep it here
39+
}
40+
}
2041
}
2142
}
2243
)
2344

24-
25-
TextField("", text: guessBinding, onCommit: {
26-
27-
})
28-
.frame(maxWidth: 40, alignment: .center)
29-
.padding([.trailing, .leading], 10)
30-
.padding([.vertical], 15)
31-
.lineLimit(1)
32-
.multilineTextAlignment(.center)
33-
.border(.black)
34-
.background(viewModel.getLetterStatusBackgroundColor(guessAttempt: guessNumber, character: character))
45+
TextField("", text: guessBinding)
46+
.textInputAutocapitalization(.characters)
47+
.disableAutocorrection(true)
48+
.font(.system(size: 20, weight: .semibold, design: .monospaced))
49+
.multilineTextAlignment(.center)
50+
.frame(width: 56, height: 56)
51+
.background(
52+
RoundedRectangle(cornerRadius: 10)
53+
.fill(viewModel.getLetterStatusBackgroundColor(guessAttempt: guessNumber, character: character))
54+
)
55+
.overlay(
56+
RoundedRectangle(cornerRadius: 10)
57+
.stroke(Color.black.opacity(0.6), lineWidth: 1)
58+
)
59+
.focused($focusedPos, equals: FocusPos(row: guessNumber, col: character))
60+
.submitLabel(.done)
61+
.onSubmit {
62+
// If current row is fully filled, trigger Guess
63+
if guessNumber == viewModel.getCurrentGuessAttempt() {
64+
let maxLetters = viewModel.getMaxNumberLetters()
65+
var filled = true
66+
for col in 0..<maxLetters {
67+
if viewModel.getGuess(guessAttempt: guessNumber, character: col).isEmpty {
68+
filled = false
69+
break
70+
}
71+
}
72+
if filled {
73+
viewModel.checkGuess()
74+
// After submitting a guess, move focus to the next row's first cell
75+
let nextRow = guessNumber + 1
76+
if nextRow < viewModel.getMaxNumberGuesses() {
77+
DispatchQueue.main.async {
78+
focusedPos = FocusPos(row: nextRow, col: 0)
79+
}
80+
}
81+
}
82+
}
83+
}
3584
}
3685
}
37-
3886
}
3987

40-
HStack {
88+
if let answer = viewModel.revealedAnswer {
89+
Text("Answer: \(answer)")
90+
.font(.system(size: 18, weight: .semibold, design: .default))
91+
.foregroundColor(.primary)
92+
}
93+
94+
HStack(spacing: 16) {
4195
Button(action: {
42-
viewModel.checkGuess()
96+
// Only submit and advance focus if the current row is filled
97+
let current = viewModel.getCurrentGuessAttempt()
98+
let maxLetters = viewModel.getMaxNumberLetters()
99+
var filled = true
100+
for col in 0..<maxLetters {
101+
if viewModel.getGuess(guessAttempt: current, character: col).isEmpty {
102+
filled = false
103+
break
104+
}
105+
}
106+
if filled {
107+
viewModel.checkGuess()
108+
let nextRow = current + 1
109+
if nextRow < viewModel.getMaxNumberGuesses() {
110+
focusedPos = FocusPos(row: nextRow, col: 0)
111+
}
112+
}
43113
}) {
44114
Text("Guess")
45115
}
46116

47117
Button(action: {
48118
viewModel.newGame()
119+
// Reset focus to the first cell
120+
focusedPos = FocusPos(row: 0, col: 0)
49121
}) {
50122
Text("New Game")
51123
}
52124

53125
}
54126
}
127+
.padding(20)
55128
.navigationBarTitle(Text("WordMaster KMP"))
129+
.onAppear {
130+
// Set initial focus to first cell
131+
focusedPos = FocusPos(row: 0, col: 0)
132+
}
133+
.onChange(of: viewModel.lastGuessCorrect) { newValue in
134+
if newValue {
135+
showWinAlert = true
136+
}
137+
}
138+
.alert("You win!", isPresented: $showWinAlert) {
139+
Button("OK") {
140+
viewModel.newGame()
141+
focusedPos = FocusPos(row: 0, col: 0)
142+
showWinAlert = false
143+
}
144+
} message: {
145+
Text("Great job guessing the word.")
146+
}
56147
}
57148
}
58149
}

iosApp/iosApp/ViewModel.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ class ViewModel: ObservableObject {
99
private let wordMasterService: WordMasterService
1010
@Published public var boardStatus: [[LetterStatus]] = []
1111
@Published public var boardGuesses: [[String]] = []
12+
@Published public var revealedAnswer: String? = nil
13+
@Published public var lastGuessCorrect: Bool = false
1214

1315
init() {
1416
let wordsPath = Bundle.main.path(forResource: "words", ofType: "txt") ?? ""
@@ -37,6 +39,26 @@ class ViewModel: ObservableObject {
3739
}
3840

3941
}
42+
Task {
43+
do {
44+
let stream = asyncSequence(for: wordMasterService.revealedAnswer)
45+
for try await data in stream {
46+
self.revealedAnswer = data as? String
47+
}
48+
} catch {
49+
print("Failed with error: \(error)")
50+
}
51+
}
52+
Task {
53+
do {
54+
let stream = asyncSequence(for: wordMasterService.lastGuessCorrect)
55+
for try await data in stream {
56+
self.lastGuessCorrect = (data as? Bool) ?? false
57+
}
58+
} catch {
59+
print("Failed with error: \(error)")
60+
}
61+
}
4062
}
4163

4264
func getMaxNumberGuesses() -> Int {
@@ -47,6 +69,9 @@ class ViewModel: ObservableObject {
4769
return Int(WordMasterService.companion.NUMBER_LETTERS)
4870
}
4971

72+
func getCurrentGuessAttempt() -> Int {
73+
return Int(wordMasterService.currentGuessAttempt)
74+
}
5075

5176
func setGuess(guessAttempt: Int, character: Int, guess: String) {
5277
wordMasterService.setGuess(guessAttempt: Int32(guessAttempt), character: Int32(character), guess: guess)

0 commit comments

Comments
 (0)