@@ -10,6 +10,7 @@ private struct PadLayoutSnapshot {
1010 let perButtonPortrait : [ String : PadGroupPosition ]
1111 let perButtonLandscape : [ String : PadGroupPosition ]
1212 let controlVisibility : [ String : Bool ]
13+ let skin : VirtualPadSkin
1314}
1415
1516struct PadLayoutEditView : View {
@@ -19,48 +20,54 @@ struct PadLayoutEditView: View {
1920 @State private var editLandscape = false
2021 @State private var undoStack : [ PadLayoutSnapshot ] = [ ]
2122 @State private var originalSnapshot : PadLayoutSnapshot ? = nil
23+ @State private var didInitializeOrientation = false
2224
2325 var body : some View {
2426 GeometryReader { geo in
2527 let isActuallyLandscape = geo. size. width > geo. size. height
26- let safeAreaTop = geo. safeAreaInsets. top
27- let safeAreaBottom = geo. safeAreaInsets. bottom
2828 let isMismatch = isActuallyLandscape != editLandscape
29- let chromeAlignment : Alignment = isActuallyLandscape ? . top : . bottom
30- ZStack {
31- // Canvas
29+ ZStack ( alignment: . top) {
30+ Color . clear
31+ . contentShape ( Rectangle ( ) )
32+ . onTapGesture { }
33+ . gesture ( DragGesture ( minimumDistance: 0 ) )
34+
3235 if isActuallyLandscape {
3336 landscapeEditorCanvas ( isEditable: !isMismatch)
3437 } else {
3538 portraitEditorCanvas ( width: geo. size. width, height: geo. size. height, isEditable: !isMismatch)
3639 }
3740
38- // Orientation mismatch overlay
39- if isMismatch {
40- orientationMismatchView ( isLandscape: isActuallyLandscape)
41- . zIndex ( 1 )
42- . allowsHitTesting ( false )
41+ VStack ( spacing: 0 ) {
42+ editorToolbar ( )
43+ . padding ( . top, max ( geo. safeAreaInsets. top, 8 ) )
44+ . padding ( . horizontal, max ( max ( geo. safeAreaInsets. leading, geo. safeAreaInsets. trailing) , 8 ) )
45+
46+ if isMismatch {
47+ orientationMismatchView ( )
48+ } else {
49+ Spacer ( minLength: 0 )
50+ }
4351 }
52+ . frame ( maxWidth: . infinity, maxHeight: . infinity, alignment: . top)
53+ . zIndex ( 2 )
4454 }
45- . overlay ( alignment: chromeAlignment) {
46- editorChrome (
47- isLandscape: isActuallyLandscape,
48- safeAreaTop: safeAreaTop,
49- safeAreaBottom: safeAreaBottom
50- )
55+ . frame ( width: geo. size. width, height: geo. size. height, alignment: . top)
56+ . onAppear {
57+ if originalSnapshot == nil {
58+ originalSnapshot = captureSnapshot ( )
59+ }
60+ if !didInitializeOrientation {
61+ editLandscape = isActuallyLandscape
62+ didInitializeOrientation = true
63+ }
5164 }
5265 }
5366 . navigationBarHidden ( true )
5467 . statusBarHidden ( )
5568 . persistentSystemOverlays ( . hidden)
56- . onAppear {
57- if originalSnapshot == nil {
58- originalSnapshot = captureSnapshot ( )
59- }
60- let bounds = UIScreen . main. bounds
61- editLandscape = bounds. width > bounds. height
62- }
6369 . onDisappear {
70+ rollbackUnsavedChanges ( )
6471 NSLog ( " @@PAD_LAYOUT@@ disappear " )
6572 NotificationCenter . default. post ( name: Notification . Name ( " ARMSX2iOSPadLayoutEditorDismissed " ) , object: nil )
6673 }
@@ -77,7 +84,7 @@ struct PadLayoutEditView: View {
7784
7885 if isEditable {
7986 customSkinPreview ( isLandscape: true , width: width, height: height)
80- editorContent ( areaW: width, areaH: height)
87+ editorControls ( areaW: width, areaH: height, isLandscape : true )
8188 }
8289 }
8390 . contentShape ( Rectangle ( ) )
@@ -100,66 +107,48 @@ struct PadLayoutEditView: View {
100107 Color . black. opacity ( 0.85 )
101108 if isEditable {
102109 customSkinPreview ( isLandscape: false , width: width, height: padHeight)
103- editorPortraitContent ( areaW: width, areaH: padHeight)
110+ editorControls ( areaW: width, areaH: padHeight, isLandscape : false )
104111 }
105112 }
106113 . frame ( height: padHeight)
107114 . clipped ( )
108115 }
109116 }
110117
111- // MARK: - Editor toolbar
112- private func editorChrome( isLandscape: Bool , safeAreaTop: CGFloat , safeAreaBottom: CGFloat ) -> some View {
113- editorToolbar ( isLandscape: isLandscape)
114- . padding ( . top, isLandscape ? max ( safeAreaTop, 8 ) : 0 )
115- . padding ( . bottom, isLandscape ? 0 : max ( safeAreaBottom, 8 ) )
116- }
117-
118- private func editorToolbar( isLandscape: Bool ) -> some View {
119- HStack ( spacing: isLandscape ? 8 : 12 ) {
120- // Cancel
118+ private func editorToolbar( ) -> some View {
119+ HStack ( spacing: 6 ) {
121120 Button {
122- if let snapshot = originalSnapshot {
123- restore ( snapshot: snapshot)
124- }
121+ rollbackUnsavedChanges ( )
125122 onDismiss ( )
126123 } label: {
127124 Image ( systemName: " xmark.circle.fill " )
128- . font ( isLandscape ? . title3 : . title2 )
125+ . font ( . title3)
129126 . foregroundStyle ( . white)
127+ . frame ( width: 28 , height: 28 )
130128 }
131129
132- // Undo
133130 Button {
134131 guard let snapshot = undoStack. popLast ( ) else { return }
135132 restore ( snapshot: snapshot)
136133 } label: {
137134 Image ( systemName: " arrow.uturn.backward.circle " )
138- . font ( isLandscape ? . title3 : . title2 )
135+ . font ( . title3)
139136 . foregroundStyle ( undoStack. isEmpty ? . white. opacity ( 0.3 ) : . white)
137+ . frame ( width: 28 , height: 28 )
140138 }
141139 . disabled ( undoStack. isEmpty)
142140
143- Spacer ( )
144-
145- // Portrait / Landscape
146141 Picker ( " " , selection: $editLandscape) {
147- Text ( " Portrait " ) . tag ( false )
148- Text ( " Landscape " ) . tag ( true )
142+ Text ( " Port. " ) . tag ( false )
143+ Text ( " Land. " ) . tag ( true )
149144 }
150145 . pickerStyle ( . segmented)
151- . frame ( width: 160 )
146+ . frame ( width: 112 )
152147 . background ( . black. opacity ( 0.3 ) , in: RoundedRectangle ( cornerRadius: 8 ) )
153148
154- Spacer ( )
155-
156- // Skin picker
157- skinPickerMenuButton ( isLandscape: isLandscape)
158-
159- // Options (visibility + reset)
160- optionsMenuButton ( isLandscape: isLandscape)
149+ skinPickerMenuButton ( )
150+ optionsMenuButton ( )
161151
162- // Save
163152 Button {
164153 NSLog ( " @@PAD_LAYOUT@@ save portrait=%d landscape=%d " , layout. portrait. count, layout. landscape. count)
165154 layout. save ( )
@@ -168,16 +157,17 @@ struct PadLayoutEditView: View {
168157 onDismiss ( )
169158 } label: {
170159 Image ( systemName: " checkmark.circle.fill " )
171- . font ( isLandscape ? . title3 : . title2 )
160+ . font ( . title3)
172161 . foregroundStyle ( . green)
162+ . frame ( width: 28 , height: 28 )
173163 }
174164 }
175- . padding ( . horizontal, 12 )
165+ . padding ( . horizontal, 8 )
176166 . padding ( . vertical, 6 )
177167 . background ( . black. opacity ( 0.55 ) , in: RoundedRectangle ( cornerRadius: 10 , style: . continuous) )
178168 }
179169
180- private func orientationMismatchView( isLandscape : Bool ) -> some View {
170+ private func orientationMismatchView( ) -> some View {
181171 let message = editLandscape
182172 ? " Rotate device to landscape to edit landscape layout "
183173 : " Rotate device to portrait to edit portrait layout "
@@ -206,25 +196,27 @@ struct PadLayoutEditView: View {
206196 . allowsHitTesting ( false )
207197 }
208198
209- private func skinPickerMenuButton( isLandscape : Bool ) -> some View {
199+ private func skinPickerMenuButton( ) -> some View {
210200 Menu {
211201 ForEach ( VirtualPadSkin . allCases) { skin in
212202 Button {
203+ guard settings. virtualPadSkin != skin else { return }
204+ pushSnapshot ( )
213205 settings. virtualPadSkin = skin
214206 } label: {
215207 Label ( settings. localized ( skin. label) , systemImage: settings. virtualPadSkin == skin ? " checkmark " : " circle " )
216208 }
217209 }
218210 } label: {
219211 Image ( systemName: " paintpalette.fill " )
220- . font ( isLandscape ? . title3 : . title2 )
212+ . font ( . title3)
221213 . foregroundStyle ( . white)
214+ . frame ( width: 28 , height: 28 )
222215 }
223216 }
224217
225- private func optionsMenuButton( isLandscape : Bool ) -> some View {
218+ private func optionsMenuButton( ) -> some View {
226219 Menu {
227- // Visibility section
228220 Section ( " Visibility " ) {
229221 ForEach ( PadLayoutStore . groupIDs. filter { $0 != " action " } , id: \. self) { ( id: String ) in
230222 visibilityMenuItem ( id: id)
@@ -235,7 +227,6 @@ struct PadLayoutEditView: View {
235227 visibilityMenuItem ( id: " square " , label: " Square " )
236228 }
237229
238- // Reset section
239230 Section ( " Reset " ) {
240231 Button ( " Reset Action " , role: . destructive) {
241232 pushSnapshot ( )
@@ -256,8 +247,9 @@ struct PadLayoutEditView: View {
256247 }
257248 } label: {
258249 Image ( systemName: " ellipsis.circle " )
259- . font ( isLandscape ? . title3 : . title2 )
250+ . font ( . title3)
260251 . foregroundStyle ( . white)
252+ . frame ( width: 28 , height: 28 )
261253 }
262254 }
263255
@@ -288,34 +280,14 @@ struct PadLayoutEditView: View {
288280 }
289281
290282 @ViewBuilder
291- private func editorContent( areaW: CGFloat , areaH: CGFloat ) -> some View {
292- let stickScale = min ( max ( CGFloat ( settings. analogStickScale) , 0.8 ) , 1.6 )
293- ZStack {
294- // Individual buttons for action and dpad
295- ForEach ( PadLayoutStore . perButtonIDs, id: \. self) { ( id: String ) in
296- DraggableButton ( id: id, areaW: areaW, areaH: areaH, isLandscape: editLandscape, onBeginEdit: pushSnapshot)
297- }
298- // Group widgets for other controls
299- ForEach ( PadLayoutStore . groupIDs. filter { $0 != " action " && $0 != " dpad " } , id: \. self) { ( id: String ) in
300- DraggableGroup ( id: id, areaW: areaW, areaH: areaH, isLandscape: editLandscape, analogStickScale: stickScale, onBeginEdit: pushSnapshot)
301- }
302- }
303- . environment ( \. padSkin, settings. virtualPadSkin)
304- . environment ( \. padOpacity, 1.0 )
305- . environment ( \. padUsesFullSkin, false )
306- }
307-
308- @ViewBuilder
309- private func editorPortraitContent( areaW: CGFloat , areaH: CGFloat ) -> some View {
283+ private func editorControls( areaW: CGFloat , areaH: CGFloat , isLandscape: Bool ) -> some View {
310284 let stickScale = min ( max ( CGFloat ( settings. analogStickScale) , 0.8 ) , 1.6 )
311285 ZStack {
312- // Individual buttons for action and dpad
313286 ForEach ( PadLayoutStore . perButtonIDs, id: \. self) { ( id: String ) in
314- DraggableButton ( id: id, areaW: areaW, areaH: areaH, isLandscape: editLandscape , onBeginEdit: pushSnapshot)
287+ DraggableButton ( id: id, areaW: areaW, areaH: areaH, isLandscape: isLandscape , onBeginEdit: pushSnapshot)
315288 }
316- // Group widgets for other controls
317289 ForEach ( PadLayoutStore . groupIDs. filter { $0 != " action " && $0 != " dpad " } , id: \. self) { ( id: String ) in
318- DraggableGroup ( id: id, areaW: areaW, areaH: areaH, isLandscape: editLandscape , analogStickScale: stickScale, onBeginEdit: pushSnapshot)
290+ DraggableGroup ( id: id, areaW: areaW, areaH: areaH, isLandscape: isLandscape , analogStickScale: stickScale, onBeginEdit: pushSnapshot)
319291 }
320292 }
321293 . environment ( \. padSkin, settings. virtualPadSkin)
@@ -331,7 +303,8 @@ struct PadLayoutEditView: View {
331303 landscape: layout. landscape,
332304 perButtonPortrait: layout. perButtonPortrait,
333305 perButtonLandscape: layout. perButtonLandscape,
334- controlVisibility: layout. controlVisibility
306+ controlVisibility: layout. controlVisibility,
307+ skin: settings. virtualPadSkin
335308 )
336309 }
337310
@@ -341,11 +314,19 @@ struct PadLayoutEditView: View {
341314 layout. perButtonPortrait = snapshot. perButtonPortrait
342315 layout. perButtonLandscape = snapshot. perButtonLandscape
343316 layout. controlVisibility = snapshot. controlVisibility
317+ settings. virtualPadSkin = snapshot. skin
344318 }
345319
346320 private func pushSnapshot( ) {
347321 undoStack. append ( captureSnapshot ( ) )
348322 }
323+
324+ private func rollbackUnsavedChanges( ) {
325+ guard let snapshot = originalSnapshot else { return }
326+ restore ( snapshot: snapshot)
327+ undoStack. removeAll ( )
328+ originalSnapshot = nil
329+ }
349330}
350331
351332// MARK: - Draggable group widget
0 commit comments