-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEditableTimerLabel.swift
130 lines (119 loc) · 5.04 KB
/
EditableTimerLabel.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//
// EditableTimerLabel.swift
// Focal
//
// Created by Niek van de Pas on 22/12/2024.
//
import SwiftUI
enum EditableTimerLabelSizeVariant {
case small
case large
}
#if os(macOS)
let timerStateLabelFontSize = 20.0
#else
let timerStateLabelFontSize = 26.0
#endif
#if os(macOS)
let timerTimeLeftFontSize = 50.0
#else
let timerTimeLeftFontSize = 64.0
#endif
struct EditableTimerLabel: View {
@Binding var timerLabel: String
var variant: EditableTimerLabelSizeVariant
@StateObject var timerViewModel = TimerViewModel.shared
@State var lastUsedRestTimerName: String = TimerViewModel.shared.restTimerName
@State var lastUsedWorkTimerName: String = TimerViewModel.shared.workTimerName
@FocusState private var isTextFieldFocused: Bool // Track focus state
init(for text: Binding<String>, variant: EditableTimerLabelSizeVariant) {
self._timerLabel = text // This binds the internal 'text' to the external 'for'
self.variant = variant
}
var body: some View {
return TextField("", text: $timerLabel)
.font(.custom("Geist Mono", size: variant == .small ? timerStateLabelFontSize : timerTimeLeftFontSize))
// For the large variant, push the label up by 20 points by setting negative padding
.padding(variant == .small ? .bottom : [], variant == .small ? -20 : 0)
.foregroundStyle(variant == .small ? .black : .primaryButton)
.textFieldStyle(PlainTextFieldStyle())
.multilineTextAlignment(.center)
.focused($isTextFieldFocused)
.onChange(of: isTextFieldFocused) { newValue in
// When focus changes (user clicks out of the TextField)
if !newValue { // This triggers when user clicks "out" of the TextField
handleFocusLost()
}
}
.onChange(of: timerLabel) { newValue in
// Uppercase characters take up more horizontal space than lowercase characters.
// To ensure the timer label does not overflow its container,
// and yet allow the user as much leniency as possible,
// we check to see if the entered label is all lowercase.
let MAX_LABEL_LENGTH_WHEN_ALL_LOWERCASE = 6
let MAX_LABEL_LENGTH_WHEN_NOT_ALL_LOWERCASE = 5
let isAllLowercase = newValue.allSatisfy { $0.isLowercase }
if isAllLowercase && newValue.count > MAX_LABEL_LENGTH_WHEN_ALL_LOWERCASE {
if timerViewModel.timerState == .rest {
timerViewModel.restTimerName = String(newValue.prefix(MAX_LABEL_LENGTH_WHEN_ALL_LOWERCASE))
}
else {
timerViewModel.workTimerName = String(newValue.prefix(MAX_LABEL_LENGTH_WHEN_ALL_LOWERCASE))
}
}
if (!isAllLowercase) && newValue.count > MAX_LABEL_LENGTH_WHEN_NOT_ALL_LOWERCASE {
if timerViewModel.timerState == .rest {
timerViewModel.restTimerName = String(newValue.prefix(MAX_LABEL_LENGTH_WHEN_NOT_ALL_LOWERCASE))
}
else {
timerViewModel.workTimerName = String(newValue.prefix(MAX_LABEL_LENGTH_WHEN_NOT_ALL_LOWERCASE))
}
}
}
.onSubmit {
// Disallow empty timer names by ignoring changes and reverting to the last timer name
if timerLabel.isEmpty || timerLabel.allSatisfy({ $0.isWhitespace }) {
switch timerViewModel.timerState {
case .work:
timerViewModel.workTimerName = lastUsedWorkTimerName
case .longRest:
fallthrough
case .rest:
timerViewModel.restTimerName = lastUsedRestTimerName
}
}
else {
switch timerViewModel.timerState {
case .work:
lastUsedWorkTimerName = timerLabel
case .longRest:
fallthrough
case .rest:
lastUsedRestTimerName = timerLabel
}
}
}
}
/// Handles focus loss from the timer label text field. If the label is empty or contains only whitespace, it restores the last used timer name based on the current timer state. Otherwise, it updates the last used timer name with the new label.
private func handleFocusLost() {
if timerLabel.isEmpty || timerLabel.allSatisfy({ $0.isWhitespace }) {
switch timerViewModel.timerState {
case .work:
timerViewModel.workTimerName = lastUsedWorkTimerName
case .longRest:
fallthrough
case .rest:
timerViewModel.restTimerName = lastUsedRestTimerName
}
} else {
switch timerViewModel.timerState {
case .work:
lastUsedWorkTimerName = timerLabel
case .longRest:
fallthrough
case .rest:
lastUsedRestTimerName = timerLabel
}
}
}
}