66preset state from TouchControlPresets.swift) on the host: the built-in
77controller layout on phone and tablet screens, built-ins read-only, "Custom
88Layout N" naming, encode/decode and what loading a layout puts on screen.
9+ Where SwiftUI is available, it also checks the production canvas hit shape.
910
1011The preset is checked as data: its name, id and its 18 bindings, each an XInput
1112mapping that produces a non-neutral pad sample; and the key sticks' glyphs.
199200let old = try! JSONDecoder().decode(ControlPreset.self, from: Data(#"{"id":"a","name":"Old","controls":[]}"#.utf8))
200201require(old.zones == nil && ControlPresetStore.zones(of: old).isEmpty, "layouts saved before zones still load")
201202
203+ __EDITOR_SIZE_TESTS__
204+
202205if failures > 0 { print("\(failures) FAILED"); exit(1) }
203206print("PASS: built-in controller layout, layout store, naming and loading")
204207'''
205208
209+ # Compile the production editor callbacks against small state fixtures.
210+ def function (source , start ):
211+ i = source .index (start ); b = source .index ('{' , i ); depth = 1 ; k = b + 1
212+ while depth :
213+ depth += (source [k ] == '{' ) - (source [k ] == '}' ); k += 1
214+ return source [i :k ]
215+
216+
217+ panel = content [content .index ('struct MappingPanel: View {' ):]
218+ setter = panel .split ('set: { value in' , 1 )[1 ].split ('}), in:' , 1 )[0 ]
219+ backdrop = content .split ('Color.clear.contentShape(editorCanvas(geo.size))' , 1 )[1 ]
220+ dismiss = function (backdrop , '.onTapGesture {' ).split ('{' , 1 )[1 ][:- 1 ]
221+ sizes = '\n ' .join (function (content , signature ) for signature in ('static func diameter(' , 'static func size(' ))
222+ canvas = function (content , 'private func editorCanvas(' )
223+ editor_tests = r'''
224+ final class EditorControls {
225+ static let shared = EditorControls()
226+ static let baseDiameter: CGFloat = 64
227+ var sizeScale = 1.0
228+ var controls: [TouchControl] = []
229+ var selected: UUID? = UUID()
230+ var editing = true
231+ func index(of id: UUID?) -> Int? { controls.firstIndex { $0.id == id } }
232+ ''' + sizes + r'''
233+ }
234+ final class EditorZones {
235+ static var enabled = true
236+ var selected: UUID? = UUID()
237+ var zones: [GestureZone] = []
238+ }
239+ func setSize(_ control: TouchControl, _ value: Double, _ m: EditorControls) {
240+ ''' + setter + r'''
241+ }
242+ func dismissPanel(_ m: EditorControls, _ zones: EditorZones) {
243+ ''' + dismiss + r'''
244+ }
245+ let editor = EditorControls()
246+ var first = TouchControl(), second = TouchControl()
247+ editor.controls = [first, second]
248+ setSize(first, 3.0, editor)
249+ require(editor.controls[0].scale == 3 && editor.controls[1].scale == 1, "size slider changes only its selected control")
250+ editor.controls[0].locked = true
251+ setSize(first, 0.5, editor)
252+ require(editor.controls[0].scale == 3, "size slider honours the current lock even with an older view value")
253+ setSize(TouchControl(), 0.5, editor)
254+ require(editor.controls.count == 2 && editor.controls[0].scale == 3, "a removed slider target is ignored")
255+ editor.controls[0].locked = nil
256+ setSize(first, 0.5, editor)
257+ let resized = try! JSONDecoder().decode([TouchControl].self, from: JSONEncoder().encode(editor.controls))
258+ require(resized[0].scale == 0.5 && resized[1].scale == 1, "slider sizes survive the controls file format")
259+ let editorZones = EditorZones(), beforeDismiss = editor.controls
260+ dismissPanel(editor, editorZones)
261+ require(editor.selected == nil && editorZones.selected == nil && editor.editing,
262+ "an outside tap dismisses either panel and keeps edit mode")
263+ require(editor.controls == beforeDismiss, "dismissing the panel preserves control edits")
264+ #if canImport(SwiftUI)
265+ typealias TouchControlsModel = EditorControls
266+ typealias GestureZonesModel = EditorZones
267+ let m = editor, zones = editorZones
268+ ''' + canvas + r'''
269+ let canvasScreen = CGSize(width: 800, height: 400)
270+ var canvasControl = TouchControl()
271+ editor.controls = [canvasControl]
272+ var canvasZone = GestureZone(); canvasZone.x = 0.4; canvasZone.w = 0.4
273+ editorZones.zones = [canvasZone]
274+ let area = editorCanvas(canvasScreen)
275+ require(!area.contains(CGPoint(x: 400, y: 200)), "a control overlapping a zone is excluded from canvas taps")
276+ require(!area.contains(CGPoint(x: 600, y: 300)), "zone taps stay with the zone editor")
277+ require(area.contains(CGPoint(x: 720, y: 380)), "empty space takes dismissal taps")
278+ EditorZones.enabled = false
279+ require(editorCanvas(canvasScreen).contains(CGPoint(x: 600, y: 300)), "disabled zones do not block dismissal")
280+ EditorZones.enabled = true; editorZones.zones = []
281+ require(editorCanvas(canvasScreen).contains(CGPoint(x: 490, y: 200)), "canvas starts beyond a normal-size control")
282+ editor.controls[0].scale = 3
283+ require(!editorCanvas(canvasScreen).contains(CGPoint(x: 490, y: 200)), "the tap exclusion follows a resized control")
284+ editor.controls[0].action = .pad("RT"); editor.controls[0].scale = 1
285+ require(!editorCanvas(canvasScreen).contains(CGPoint(x: 445, y: 200)), "wide shoulder controls keep their full touch region")
286+ #endif
287+ '''
288+ tests = tests .replace ('__EDITOR_SIZE_TESTS__' , editor_tests )
289+
206290with tempfile .TemporaryDirectory (prefix = 'madeira-presets-' ) as tmp :
207291 src , exe = Path (tmp ) / 'main.swift' , Path (tmp ) / 'check'
208292 # CoreGraphics where it exists: on macOS, Foundation alone no longer gives CGRect
209293 # its members (midX, width, ...); Linux's Foundation still does.
210- header = 'import Foundation\n #if canImport(CoreGraphics)\n import CoreGraphics\n #endif\n '
294+ header = 'import Foundation\n #if canImport(CoreGraphics)\n import CoreGraphics\n #endif\n #if canImport(SwiftUI) \n import SwiftUI \n #endif \n '
211295 src .write_text (header + actions + pure_touch + pure + tests )
212296 subprocess .run ([os .environ .get ('SWIFTC' , 'swiftc' ), str (src ), '-o' , str (exe )], check = True )
213297 subprocess .run ([str (exe )], check = True )
214298
215299# ---------------------------------------------------------------- Part B
216- def function (source , start ):
217- i = source .index (start ); b = source .index ('{' , i ); depth = 1 ; k = b + 1
218- while depth :
219- depth += (source [k ] == '{' ) - (source [k ] == '}' ); k += 1
220- return source [i :k ]
221-
222300gamepad = (app / 'GamepadInput.swift' ).read_text ()
223301for flag , where in (('MADEIRA_CONTROL_PRESETS' , presets ), ('MADEIRA_CONTROLS_EDITOR_DONE' , content )):
224302 assert f'.flag("{ flag } ")' in where , flag + ' is a kill switch'
@@ -240,6 +318,8 @@ def function(source, start):
240318assert 'm.controls.isEmpty' in default and 'm.needsDefaultLayout = false' in default , 'applied once, never over controls'
241319assert 'Self.touchPad' in function (presets , 'var defaultPending: Bool {' ), 'no controller default without touch XInput'
242320overlay = function (content , 'struct TouchControlsOverlay: View {' )
321+ assert overlay .index ('Color.clear.contentShape(editorCanvas(geo.size))' ) < overlay .index ('GestureZonesLayer(' ), 'dismissal stays behind editable controls'
322+ assert panel .index ('.onTapGesture { }' ) < panel .index ('.position(layout.center)' ), 'only the panel consumes its background taps'
243323assert 'ControlPresetsModel.enabled && m.visible && !m.editing' in overlay , 'menu only while touch controls are shown'
244324assert 'if m.editing && Self.editorDone {' in overlay and 'Text("Done")' in overlay
245325assert 'TouchControlsOverlay.showsLayoutMenu(self) ? 3 : 2' in model , 'the menu button is hit-testable'
0 commit comments