fix: compact, aligned controller face layout

Group each stick above its cluster (L over D-pad, R over face
buttons) in equal-height columns so the rows line up, cap the face
at a natural controller proportion so wide cards do not stretch it,
and replace the Select/Start chips with small round icon buttons
flanking the centered Guide button.

Co-Authored-By: qwen3.8-27b@q3_k_xl: compacted and aligned the face
This commit is contained in:
portersky
2026-08-29 15:02:01 +02:00
parent a652a9f187
commit 33356afe47
3 changed files with 63 additions and 32 deletions
+39 -31
View File
@@ -1,19 +1,23 @@
import SwiftUI
// Card for one connected controller. The layout mirrors the physical Xbox
// controller face: shoulders (triggers + bumpers) along the top, sticks side
// by side below them, D-pad lower left, face buttons lower right, and the
// Guide button centered between the two clusters flanked by Select and Start.
// Card for one connected controller. The face mirrors the physical Xbox
// controller: shoulders (triggers + bumpers) along the top, left stick above
// the D-pad, right stick above the face buttons, and the Guide button
// centered between the two columns flanked by Select and Start. The face is
// capped at a natural controller proportion so wide cards do not stretch it.
struct ControllerCardView: View {
let controller: ControllerInput
var body: some View {
VStack(alignment: .leading, spacing: 14) {
VStack(alignment: .leading, spacing: 12) {
header
shoulders
sticks
clusters
VStack(spacing: 12) {
shoulders
face
}
.frame(maxWidth: 420)
.frame(maxWidth: .infinity)
footer
}
.padding(14)
@@ -50,30 +54,34 @@ struct ControllerCardView: View {
}
}
// Sticks sit side by side in the upper center of the face. A stick click
// (LS/RS) lights up its ring.
private var sticks: some View {
HStack(spacing: 48) {
StickPadView(label: "L", x: controller.stickLeftX, y: controller.stickLeftY,
pressed: controller.isPressed(.leftStick))
StickPadView(label: "R", x: controller.stickRightX, y: controller.stickRightY,
pressed: controller.isPressed(.rightStick))
}
.frame(maxWidth: .infinity)
}
// Lower clusters with the system buttons centered between them.
private var clusters: some View {
HStack(alignment: .center, spacing: 12) {
DPadView(controller: controller)
Spacer()
HStack(spacing: 10) {
ButtonChip(button: .select, pressed: controller.isPressed(.select))
GuideButtonView(pressed: controller.guideDown)
ButtonChip(button: .start, pressed: controller.isPressed(.start))
// The face: two equal-width columns (stick over cluster) with the system
// buttons centered between them. Both columns are the same height, so
// sticks line up and clusters line up.
private var face: some View {
HStack(alignment: .center, spacing: 0) {
VStack(spacing: 12) {
StickPadView(label: "L", x: controller.stickLeftX, y: controller.stickLeftY,
pressed: controller.isPressed(.leftStick))
DPadView(controller: controller)
.frame(height: 104)
}
Spacer()
FaceButtonsView(controller: controller)
.frame(maxWidth: .infinity)
HStack(spacing: 8) {
SystemButtonView(button: .select, symbol: "square.on.square",
pressed: controller.isPressed(.select))
GuideButtonView(pressed: controller.guideDown)
SystemButtonView(button: .start, symbol: "line.3.horizontal",
pressed: controller.isPressed(.start))
}
VStack(spacing: 12) {
StickPadView(label: "R", x: controller.stickRightX, y: controller.stickRightY,
pressed: controller.isPressed(.rightStick))
FaceButtonsView(controller: controller)
.frame(height: 104)
}
.frame(maxWidth: .infinity)
}
}
+23
View File
@@ -125,6 +125,29 @@ struct TriggerBarView: View {
}
}
// Small round system buttons flanking the Guide button (Select and Start).
struct SystemButtonView: View {
let button: XboxButton
let symbol: String
let pressed: Bool
var body: some View {
VStack(spacing: 4) {
ZStack {
Circle()
.fill(pressed ? Color.green : Color.secondary.opacity(0.25))
Image(systemName: symbol)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(pressed ? .white : .secondary)
}
.frame(width: 24, height: 24)
Text(button.label)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
// The large Guide button, centered between the D-pad and face buttons.
struct GuideButtonView: View {
let pressed: Bool