@@ -10,12 +10,12 @@ struct ScaleView: View {
1010 @State private var scaleCompression : CGFloat = 0
1111 @State private var displayShake = false
1212 @State private var particleOffset : CGFloat = 0
13+ @State private var keyMonitor : Any ?
1314
1415 var body : some View {
15- if #available( macOS 14 . 0 , * ) {
16- GeometryReader { geometry in
17- ZStack {
18- // Animated gradient background
16+ GeometryReader { geometry in
17+ ZStack {
18+ // Animated gradient background
1919// LinearGradient(
2020// colors: [
2121// Color(red: 0.95, green: 0.97, blue: 1.0),
@@ -25,115 +25,125 @@ struct ScaleView: View {
2525// endPoint: .bottomTrailing
2626// )
2727// .ignoresSafeArea()
28-
29- VStack ( spacing: geometry. size. height * 0.06 ) {
30- // Title with subtitle directly underneath
31- VStack ( spacing: 8 ) {
32- Text ( " Track Weight " )
33- . font ( . system( size: min ( max ( geometry. size. width * 0.05 , 24 ) , 42 ) , weight: . bold, design: . rounded) )
34- . foregroundStyle (
35- LinearGradient (
36- colors: [ . blue, . teal, . cyan] ,
37- startPoint: . leading,
38- endPoint: . trailing
39- )
28+
29+ VStack ( spacing: geometry. size. height * 0.06 ) {
30+ // Title with subtitle directly underneath
31+ VStack ( spacing: 8 ) {
32+ Text ( " Track Weight " )
33+ . font ( . system( size: min ( max ( geometry. size. width * 0.05 , 24 ) , 42 ) , weight: . bold, design: . rounded) )
34+ . foregroundStyle (
35+ LinearGradient (
36+ colors: [ . blue, . teal, . cyan] ,
37+ startPoint: . leading,
38+ endPoint: . trailing
4039 )
41- . minimumScaleFactor ( 0.7 )
42- . lineLimit ( 1 )
43-
44- Text ( " Place your finger on the trackpad to begin " )
45- . font ( . system( size: min ( max ( geometry. size. width * 0.022 , 14 ) , 18 ) , weight: . medium) )
46- . foregroundStyle ( . gray)
47- . multilineTextAlignment ( . center)
48- . frame ( maxWidth: geometry. size. width * 0.8 )
49- . opacity ( viewModel. hasTouch ? 0 : 1 )
50- . animation ( . easeInOut( duration: 0.5 ) , value: viewModel. hasTouch)
51- }
52- . frame ( height: max ( geometry. size. height * 0.15 , 80 ) ) // Fixed height for title + subtitle
53- . frame ( maxWidth: . infinity) // Ensure full width for centering
54-
55- Spacer ( )
56-
57- // Cartoon Digital Scale - responsive size
58- HStack {
59- Spacer ( )
60- CartoonScaleView (
61- weight: viewModel. currentWeight,
62- hasTouch: viewModel. hasTouch,
63- compression: $scaleCompression,
64- displayShake: $displayShake,
65- scaleFactor: min ( geometry. size. width / 700 , geometry. size. height / 500 )
6640 )
67- Spacer ( )
68- }
41+ . minimumScaleFactor ( 0.7 )
42+ . lineLimit ( 1 )
6943
44+ Text ( " Place your finger on the trackpad to begin " )
45+ . font ( . system( size: min ( max ( geometry. size. width * 0.022 , 14 ) , 18 ) , weight: . medium) )
46+ . foregroundStyle ( . gray)
47+ . multilineTextAlignment ( . center)
48+ . frame ( maxWidth: geometry. size. width * 0.8 )
49+ . opacity ( viewModel. hasTouch ? 0 : 1 )
50+ . animation ( . easeInOut( duration: 0.5 ) , value: viewModel. hasTouch)
51+ }
52+ . frame ( height: max ( geometry. size. height * 0.15 , 80 ) ) // Fixed height for title + subtitle
53+ . frame ( maxWidth: . infinity) // Ensure full width for centering
54+
55+ Spacer ( )
56+
57+ // Cartoon Digital Scale - responsive size
58+ HStack {
7059 Spacer ( )
60+ CartoonScaleView (
61+ weight: viewModel. currentWeight,
62+ hasTouch: viewModel. hasTouch,
63+ compression: $scaleCompression,
64+ displayShake: $displayShake,
65+ scaleFactor: min ( geometry. size. width / 700 , geometry. size. height / 500 )
66+ )
67+ Spacer ( )
68+ }
69+
70+ Spacer ( )
71+
72+ // Fixed container for button to prevent jumping
73+ VStack ( spacing: 10 ) {
74+ if viewModel. hasTouch {
75+ Text ( " Press spacebar or click to zero " )
76+ . font ( . system( size: min ( max ( geometry. size. width * 0.018 , 12 ) , 16 ) , weight: . medium) )
77+ . foregroundStyle ( . gray)
78+ }
7179
72- // Fixed container for button to prevent jumping
73- VStack ( spacing: 10 ) {
74- if viewModel. hasTouch {
75- Text ( " Press spacebar or click to zero " )
76- . font ( . system( size: min ( max ( geometry. size. width * 0.018 , 12 ) , 16 ) , weight: . medium) )
77- . foregroundStyle ( . gray)
80+ Button ( action: {
81+ viewModel. zeroScale ( )
82+ } ) {
83+ HStack ( spacing: 8 ) {
84+ Image ( systemName: " arrow.clockwise " )
85+ . font ( . system( size: min ( max ( geometry. size. width * 0.02 , 14 ) , 18 ) , weight: . semibold) )
86+ Text ( " Zero Scale " )
87+ . font ( . system( size: min ( max ( geometry. size. width * 0.02 , 14 ) , 18 ) , weight: . semibold) )
7888 }
79-
80- Button ( action: {
81- viewModel. zeroScale ( )
82- } ) {
83- HStack ( spacing: 8 ) {
84- Image ( systemName: " arrow.clockwise " )
85- . font ( . system( size: min ( max ( geometry. size. width * 0.02 , 14 ) , 18 ) , weight: . semibold) )
86- Text ( " Zero Scale " )
87- . font ( . system( size: min ( max ( geometry. size. width * 0.02 , 14 ) , 18 ) , weight: . semibold) )
88- }
89- . foregroundStyle ( . white)
90- . frame ( width: min ( max ( geometry. size. width * 0.2 , 140 ) , 180 ) ,
91- height: min ( max ( geometry. size. height * 0.08 , 40 ) , 55 ) )
92- . background (
93- RoundedRectangle ( cornerRadius: 25 )
94- . fill (
95- LinearGradient (
96- colors: [ . blue, . teal] ,
97- startPoint: . leading,
98- endPoint: . trailing
99- )
89+ . foregroundStyle ( . white)
90+ . frame ( width: min ( max ( geometry. size. width * 0.2 , 140 ) , 180 ) ,
91+ height: min ( max ( geometry. size. height * 0.08 , 40 ) , 55 ) )
92+ . background (
93+ RoundedRectangle ( cornerRadius: 25 )
94+ . fill (
95+ LinearGradient (
96+ colors: [ . blue, . teal] ,
97+ startPoint: . leading,
98+ endPoint: . trailing
10099 )
101- )
102- }
103- . buttonStyle ( . plain)
104- . opacity ( viewModel. hasTouch ? 1 : 0 )
105- . scaleEffect ( viewModel. hasTouch ? 1 : 0.8 )
106- . animation ( . spring( response: 0.4 , dampingFraction: 0.8 ) , value: viewModel. hasTouch)
100+ )
101+ )
107102 }
108- . frame ( height: min ( max ( geometry. size. height * 0.15 , 80 ) , 100 ) ) // Fixed space for button + instruction
109- . frame ( maxWidth: . infinity) // Ensure full width for centering
103+ . buttonStyle ( . plain)
104+ . opacity ( viewModel. hasTouch ? 1 : 0 )
105+ . scaleEffect ( viewModel. hasTouch ? 1 : 0.8 )
106+ . animation ( . spring( response: 0.4 , dampingFraction: 0.8 ) , value: viewModel. hasTouch)
110107 }
111- . padding ( . horizontal, max ( geometry. size. width * 0.05 , 20 ) )
112- . padding ( . vertical, max ( geometry. size. height * 0.03 , 20 ) )
113- . frame ( maxWidth: . infinity, maxHeight: . infinity) // Ensure the VStack takes full available space
114- }
115- }
116- . focusable ( )
117- . focusEffectDisabled ( )
118- . onKeyPress ( . space) {
119- if viewModel. hasTouch {
120- viewModel. zeroScale ( )
121- }
122- return . handled
123- }
124- . onChange ( of: viewModel. currentWeight) { _, newWeight in
125- withAnimation ( . spring( response: 0.4 , dampingFraction: 0.8 ) ) {
126- scaleCompression = CGFloat ( min ( newWeight / 100.0 , 0.2 ) )
108+ . frame ( height: min ( max ( geometry. size. height * 0.15 , 80 ) , 100 ) ) // Fixed space for button + instruction
109+ . frame ( maxWidth: . infinity) // Ensure full width for centering
127110 }
111+ . padding ( . horizontal, max ( geometry. size. width * 0.05 , 20 ) )
112+ . padding ( . vertical, max ( geometry. size. height * 0.03 , 20 ) )
113+ . frame ( maxWidth: . infinity, maxHeight: . infinity) // Ensure the VStack takes full available space
128114 }
129- . onAppear {
130- viewModel. startListening ( )
115+ }
116+ . focusable ( )
117+ . modifier ( FocusEffectModifier ( ) )
118+ . onChange ( of: viewModel. currentWeight) { newWeight in
119+ withAnimation ( . spring( response: 0.4 , dampingFraction: 0.8 ) ) {
120+ scaleCompression = CGFloat ( min ( newWeight / 100.0 , 0.2 ) )
131121 }
132- . onDisappear {
133- viewModel. stopListening ( )
122+ }
123+ . onAppear {
124+ viewModel. startListening ( )
125+ setupKeyMonitoring ( )
126+ }
127+ . onDisappear {
128+ viewModel. stopListening ( )
129+ removeKeyMonitoring ( )
130+ }
131+ }
132+
133+ private func setupKeyMonitoring( ) {
134+ keyMonitor = NSEvent . addLocalMonitorForEvents ( matching: . keyDown) { event in
135+ // Space key code is 49
136+ if event. keyCode == 49 && viewModel. hasTouch {
137+ viewModel. zeroScale ( )
134138 }
135- } else {
136- // Fallback on earlier versions
139+ return event
140+ }
141+ }
142+
143+ private func removeKeyMonitoring( ) {
144+ if let monitor = keyMonitor {
145+ NSEvent . removeMonitor ( monitor)
146+ keyMonitor = nil
137147 }
138148 }
139149}
@@ -265,6 +275,16 @@ struct CartoonScaleView: View {
265275 }
266276}
267277
278+ struct FocusEffectModifier : ViewModifier {
279+ func body( content: Content ) -> some View {
280+ if #available( macOS 14 . 0 , * ) {
281+ content. focusEffectDisabled ( )
282+ } else {
283+ content
284+ }
285+ }
286+ }
287+
268288#Preview {
269289 ScaleView ( )
270290}
0 commit comments