Skip to content

Commit ec5d78d

Browse files
committed
Fix iOS layout editor chrome and state rollback
1 parent 7321d8e commit ec5d78d

2 files changed

Lines changed: 99 additions & 106 deletions

File tree

app/src/main/swift/Views/Settings/PadLayoutEditView.swift

Lines changed: 69 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1516
struct 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

app/src/main/swift/Views/VirtualControllerView.swift

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -701,24 +701,36 @@ struct VirtualControllerView: View {
701701
let cW = cGeo.size.width
702702
let cH = cGeo.size.height
703703

704-
PadBtn(label: "L2", w: 110, h: 40, btn: .L2)
705-
.scaleEffect(pos("l2", landscape: false).scale)
706-
.position(x: pos("l2", landscape: false).x * cW, y: pos("l2", landscape: false).y * cH)
707-
PadBtn(label: "L1", w: 100, h: 30, btn: .L1)
708-
.scaleEffect(pos("l1", landscape: false).scale)
709-
.position(x: pos("l1", landscape: false).x * cW, y: pos("l1", landscape: false).y * cH)
710-
PadBtn(label: "R2", w: 110, h: 40, btn: .R2)
711-
.scaleEffect(pos("r2", landscape: false).scale)
712-
.position(x: pos("r2", landscape: false).x * cW, y: pos("r2", landscape: false).y * cH)
713-
PadBtn(label: "R1", w: 100, h: 30, btn: .R1)
714-
.scaleEffect(pos("r1", landscape: false).scale)
715-
.position(x: pos("r1", landscape: false).x * cW, y: pos("r1", landscape: false).y * cH)
716-
PadBtn(label: "SEL", w: 42, h: 22, btn: .select)
717-
.scaleEffect(pos("select", landscape: false).scale)
718-
.position(x: pos("select", landscape: false).x * cW, y: pos("select", landscape: false).y * cH)
719-
PadBtn(label: "START", w: 48, h: 22, btn: .start)
720-
.scaleEffect(pos("start", landscape: false).scale)
721-
.position(x: pos("start", landscape: false).x * cW, y: pos("start", landscape: false).y * cH)
704+
if layout.isControlVisible("l2") {
705+
PadBtn(label: "L2", w: 110, h: 40, btn: .L2)
706+
.scaleEffect(pos("l2", landscape: false).scale)
707+
.position(x: pos("l2", landscape: false).x * cW, y: pos("l2", landscape: false).y * cH)
708+
}
709+
if layout.isControlVisible("l1") {
710+
PadBtn(label: "L1", w: 100, h: 30, btn: .L1)
711+
.scaleEffect(pos("l1", landscape: false).scale)
712+
.position(x: pos("l1", landscape: false).x * cW, y: pos("l1", landscape: false).y * cH)
713+
}
714+
if layout.isControlVisible("r2") {
715+
PadBtn(label: "R2", w: 110, h: 40, btn: .R2)
716+
.scaleEffect(pos("r2", landscape: false).scale)
717+
.position(x: pos("r2", landscape: false).x * cW, y: pos("r2", landscape: false).y * cH)
718+
}
719+
if layout.isControlVisible("r1") {
720+
PadBtn(label: "R1", w: 100, h: 30, btn: .R1)
721+
.scaleEffect(pos("r1", landscape: false).scale)
722+
.position(x: pos("r1", landscape: false).x * cW, y: pos("r1", landscape: false).y * cH)
723+
}
724+
if layout.isControlVisible("select") {
725+
PadBtn(label: "SEL", w: 42, h: 22, btn: .select)
726+
.scaleEffect(pos("select", landscape: false).scale)
727+
.position(x: pos("select", landscape: false).x * cW, y: pos("select", landscape: false).y * cH)
728+
}
729+
if layout.isControlVisible("start") {
730+
PadBtn(label: "START", w: 48, h: 22, btn: .start)
731+
.scaleEffect(pos("start", landscape: false).scale)
732+
.position(x: pos("start", landscape: false).x * cW, y: pos("start", landscape: false).y * cH)
733+
}
722734

723735
// D-pad buttons (individual placement)
724736
if layout.isControlVisible("dpad") {

0 commit comments

Comments
 (0)