feat: redesign UI with gamepad-style input monitor

Split the single-file Swift app into model, store, and view files
and rework the layout. The dongle card shows a status pill with an
adaptive info grid; controllers render as cards in an adaptive grid
that fills wider windows; each card uses a gamepad-style
visualization (D-pad, face buttons, stick pads, triggers) instead of
flat indicator rows. The window is now resizable and content reflows
instead of staying a fixed 420pt column.

Co-Authored-By: qwen3.8-27b@q3_k_xl: redesigned app UI and structure
This commit is contained in:
portersky
2026-08-29 14:47:41 +02:00
parent dfe7e8b87d
commit 4931bf5c18
8 changed files with 579 additions and 305 deletions
+7 -1
View File
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
endif()
cmake_minimum_required(VERSION 3.21)
project(xone_macos VERSION 0.1.20 LANGUAGES CXX Swift)
project(xone_macos VERSION 0.1.21 LANGUAGES CXX Swift)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -93,6 +93,12 @@ target_link_libraries(xone_api PUBLIC xone_hid)
# ==============================================================================
add_executable(xone_app
"src/app/main.swift"
"src/app/XoneModels.swift"
"src/app/XoneStore.swift"
"src/app/ContentView.swift"
"src/app/DongleCardView.swift"
"src/app/ControllerCardView.swift"
"src/app/GamepadViews.swift"
)
target_include_directories(xone_app PRIVATE "${CMAKE_SOURCE_DIR}/include")
target_compile_definitions(xone_app PRIVATE ${BASE_DEFINITIONS})
+63
View File
@@ -0,0 +1,63 @@
import SwiftUI
struct ContentView: View {
@StateObject private var store = XoneStore()
// Cards flow into extra columns as the window is widened, so dead space
// is used instead of stretching a single column.
private let cardColumns = [GridItem(.adaptive(minimum: 320), spacing: 16)]
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
header
DongleCardView(store: store)
controllersSection
}
.padding(20)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.frame(minWidth: 480, idealWidth: 560, maxWidth: .infinity,
minHeight: 420, idealHeight: 640, maxHeight: .infinity)
}
private var header: some View {
HStack(spacing: 10) {
Image(systemName: "gamecontroller.fill")
.font(.title)
Text("xone_macos")
.font(.title2.bold())
Spacer()
Text("v\(String(cString: xone_version()))")
.font(.caption)
.foregroundStyle(.secondary)
}
}
private var controllersSection: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Controllers")
.font(.headline)
if store.controllers.isEmpty {
emptyState
} else {
LazyVGrid(columns: cardColumns, alignment: .leading, spacing: 16) {
ForEach(store.controllers) { controller in
ControllerCardView(controller: controller)
}
}
}
}
}
private var emptyState: some View {
Text(store.donglePresent
? "No controllers connected. Use Pair Controller to add one."
: "Plug in the dongle and pair a controller.")
.font(.callout)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, minHeight: 60)
.background(Color.secondary.opacity(0.06))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
+66
View File
@@ -0,0 +1,66 @@
import SwiftUI
struct ControllerCardView: View {
let controller: ControllerInput
var body: some View {
VStack(alignment: .leading, spacing: 12) {
header
HStack(spacing: 28) {
DPadView(controller: controller)
FaceButtonsView(controller: controller)
}
.frame(maxWidth: .infinity)
sticksAndTriggers
systemButtons
footer
}
.padding(14)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
private var header: some View {
HStack(spacing: 8) {
Circle()
.fill(controller.gipReady ? Color.green : Color.orange)
.frame(width: 8, height: 8)
Text(controller.id)
.font(.system(.callout, design: .monospaced))
Spacer()
Text(controller.gipReady ? "ready" : "connecting")
.font(.caption.weight(.medium))
.foregroundStyle(controller.gipReady ? .green : .orange)
}
}
private var sticksAndTriggers: some View {
HStack(alignment: .top, spacing: 12) {
StickPadView(label: "L", x: controller.stickLeftX, y: controller.stickLeftY)
Spacer()
TriggerBarView(label: "LT", value: controller.triggerLeft)
TriggerBarView(label: "RT", value: controller.triggerRight)
Spacer()
StickPadView(label: "R", x: controller.stickRightX, y: controller.stickRightY)
}
}
private var systemButtons: some View {
HStack(spacing: 6) {
ButtonChip(button: .guide, pressed: controller.guideDown)
ButtonChip(button: .start, pressed: controller.isPressed(.start))
ButtonChip(button: .select, pressed: controller.isPressed(.select))
Spacer()
ButtonChip(button: .leftBumper, pressed: controller.isPressed(.leftBumper))
ButtonChip(button: .rightBumper, pressed: controller.isPressed(.rightBumper))
ButtonChip(button: .leftStick, pressed: controller.isPressed(.leftStick))
ButtonChip(button: .rightStick, pressed: controller.isPressed(.rightStick))
}
}
private var footer: some View {
Text(controller.inputActive ? "input #\(controller.sequence)" : "waiting for input")
.font(.caption)
.foregroundStyle(.secondary)
}
}
+98
View File
@@ -0,0 +1,98 @@
import SwiftUI
struct DongleCardView: View {
@ObservedObject var store: XoneStore
private let infoColumns = [GridItem(.adaptive(minimum: 150), spacing: 8)]
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
StatePill(state: store.state, active: store.donglePresent)
if !store.errorText.isEmpty {
Text(store.errorText)
.font(.callout)
.foregroundStyle(.red)
.lineLimit(2)
}
Spacer()
pairingButton
}
if store.donglePresent {
LazyVGrid(columns: infoColumns, alignment: .leading, spacing: 8) {
InfoChip(label: "PID", value: String(format: "0x%04X", store.pid))
InfoChip(label: "Chip ID", value: String(format: "0x%04X", store.chipID))
InfoChip(label: "MAC", value: store.macAddress)
if store.state == .ready {
InfoChip(label: "Firmware", value: firmwareText)
}
}
} else {
Text("No dongle detected. Plug in the Xbox Wireless Dongle.")
.font(.callout)
.foregroundStyle(.secondary)
}
}
.padding(14)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
private var pairingButton: some View {
Button(store.pairingActive ? "Stop Pairing" : "Pair Controller") {
store.togglePairing()
}
.disabled(!store.donglePresent || store.state != .ready)
}
private var firmwareText: String {
store.firmwareBuild.isEmpty ? "not loaded" : store.firmwareBuild
}
}
struct StatePill: View {
let state: DongleState
let active: Bool
var body: some View {
HStack(spacing: 6) {
Circle()
.fill(color)
.frame(width: 8, height: 8)
Text(active ? state.label : "no dongle")
.font(.callout.weight(.medium))
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
.background(color.opacity(0.15))
.clipShape(Capsule())
}
private var color: Color {
guard active else { return .gray }
switch state {
case .idle: return .gray
case .starting: return .orange
case .ready: return .green
case .error: return .red
}
}
}
struct InfoChip: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(label.uppercased())
.font(.caption2)
.foregroundStyle(.secondary)
Text(value)
.font(.system(.callout, design: .monospaced))
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
+137
View File
@@ -0,0 +1,137 @@
import SwiftUI
// Gamepad-style visualization of a controller input snapshot: D-pad and face
// buttons on top, stick pads and triggers below, mirroring the physical
// layout of an Xbox controller.
struct DPadView: View {
let controller: ControllerInput
var body: some View {
VStack(spacing: 3) {
cell(.dPadUp)
HStack(spacing: 3) {
cell(.dPadLeft)
RoundedRectangle(cornerRadius: 5)
.fill(Color.secondary.opacity(0.28))
.frame(width: 30, height: 26)
cell(.dPadRight)
}
cell(.dPadDown)
}
}
private func cell(_ button: XboxButton) -> some View {
let pressed = controller.isPressed(button)
return RoundedRectangle(cornerRadius: 5)
.fill(pressed ? Color.green : Color.secondary.opacity(0.18))
.frame(width: 30, height: 26)
.overlay(
Image(systemName: button.dPadSymbol ?? "")
.font(.system(size: 9))
.foregroundStyle(pressed ? .white : .secondary)
)
}
}
struct FaceButtonsView: View {
let controller: ControllerInput
var body: some View {
VStack(spacing: 4) {
faceButton(.y)
HStack(spacing: 10) {
faceButton(.x)
faceButton(.b)
}
faceButton(.a)
}
}
private func faceButton(_ button: XboxButton) -> some View {
let pressed = controller.isPressed(button)
return Text(button.label)
.font(.callout.weight(.semibold))
.frame(width: 32, height: 32)
.background(Circle().fill(pressed ? Color.green : Color.secondary.opacity(0.18)))
.foregroundStyle(pressed ? .white : .primary)
}
}
struct StickPadView: View {
let label: String
let x: Int16
let y: Int16
private let diameter: CGFloat = 64
var body: some View {
VStack(spacing: 4) {
ZStack {
Circle()
.fill(Color.secondary.opacity(0.15))
Circle()
.stroke(Color.secondary.opacity(0.35), lineWidth: 1)
.padding(6)
Circle()
.fill(Color.accentColor)
.frame(width: 12, height: 12)
.offset(x: offset.width, y: offset.height)
}
.frame(width: diameter, height: diameter)
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
// Map the signed 16-bit stick values onto the pad, keeping the dot inside
// the ring.
private var offset: CGSize {
let travel = diameter / 2 - 10
return CGSize(
width: CGFloat(Double(x) / 32768.0 * travel),
height: CGFloat(Double(y) / 32768.0 * travel)
)
}
}
struct TriggerBarView: View {
let label: String
let value: UInt16
private let barWidth: CGFloat = 56
var body: some View {
VStack(spacing: 4) {
ZStack(alignment: .leading) {
Color.secondary.opacity(0.18)
Color.accentColor
.frame(width: barWidth * fraction, alignment: .leading)
}
.frame(width: barWidth, height: 6)
.clipShape(RoundedRectangle(cornerRadius: 3))
Text(label)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
private var fraction: CGFloat {
min(max(CGFloat(Double(value) / 1023.0), 0), 1)
}
}
struct ButtonChip: View {
let button: XboxButton
let pressed: Bool
var body: some View {
Text(button.label)
.font(.caption2.weight(.medium))
.frame(minWidth: 30, minHeight: 20)
.background(pressed ? Color.green : Color.secondary.opacity(0.15))
.foregroundStyle(pressed ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
}
+99
View File
@@ -0,0 +1,99 @@
import Foundation
// Session states reported by xone_state(). Raw values match the C enum.
enum DongleState: Int {
case idle = 0
case starting
case ready
case error
var label: String {
switch self {
case .idle: return "idle"
case .starting: return "starting"
case .ready: return "ready"
case .error: return "error"
}
}
}
// Snapshot of the latest standard Xbox controller input report. Stick values
// are signed 16-bit; trigger values are unsigned 10-bit.
struct ControllerInput: Identifiable {
let id: String
let buttons: UInt16
let guideDown: Bool
let gipReady: Bool
let inputActive: Bool
let triggerLeft: UInt16
let triggerRight: UInt16
let stickLeftX: Int16
let stickLeftY: Int16
let stickRightX: Int16
let stickRightY: Int16
let sequence: UInt32
func isPressed(_ button: XboxButton) -> Bool {
(buttons & button.mask) != 0
}
}
// Standard GIP gamepad buttons and their bit masks. Guide is a separate flag
// in the C API, so its mask is zero.
enum XboxButton {
case a, b, x, y
case dPadUp, dPadDown, dPadLeft, dPadRight
case leftBumper, rightBumper, leftStick, rightStick
case guide, start, select
var mask: UInt16 {
switch self {
case .a: return 0x0010
case .b: return 0x0020
case .x: return 0x0040
case .y: return 0x0080
case .dPadUp: return 0x0100
case .dPadDown: return 0x0200
case .dPadLeft: return 0x0400
case .dPadRight: return 0x0800
case .leftBumper: return 0x1000
case .rightBumper: return 0x2000
case .leftStick: return 0x4000
case .rightStick: return 0x8000
case .start: return 0x0004
case .select: return 0x0008
case .guide: return 0
}
}
var label: String {
switch self {
case .a: return "A"
case .b: return "B"
case .x: return "X"
case .y: return "Y"
case .dPadUp: return "Up"
case .dPadDown: return "Down"
case .dPadLeft: return "Left"
case .dPadRight: return "Right"
case .leftBumper: return "LB"
case .rightBumper: return "RB"
case .leftStick: return "LS"
case .rightStick: return "RS"
case .guide: return "Guide"
case .start: return "Start"
case .select: return "Select"
}
}
// SF Symbol for the D-pad cells; nil for every other button.
var dPadSymbol: String? {
switch self {
case .dPadUp: return "arrowtriangle.up.fill"
case .dPadDown: return "arrowtriangle.down.fill"
case .dPadLeft: return "arrowtriangle.left.fill"
case .dPadRight: return "arrowtriangle.right.fill"
default: return nil
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import AppKit
import Combine
import Foundation
// Observable model that owns the C API session and polls it on a timer.
// Views render the published state; none of them touch the C API directly.
final class XoneStore: ObservableObject {
@Published private(set) var donglePresent = false
@Published private(set) var state: DongleState = .idle
@Published private(set) var pid: UInt16 = 0
@Published private(set) var chipID: UInt16 = 0
@Published private(set) var macAddress = ""
@Published private(set) var firmwareBuild = ""
@Published private(set) var errorText = ""
@Published private(set) var controllers: [ControllerInput] = []
@Published private(set) var pairingActive = false
private var session: OpaquePointer?
private var cancellables = Set<AnyCancellable>()
init() {
// Probe is fast; firmware load + radio init run on a background
// thread in the C layer so the UI never blocks.
Timer.publish(every: 0.02, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in self?.poll() }
.store(in: &cancellables)
}
func togglePairing() {
guard let s = session, state == .ready else { return }
pairingActive.toggle()
xone_set_pairing(s, pairingActive)
}
private func poll() {
let present = xone_dongle_present()
donglePresent = present
if present && session == nil {
if let s = xone_open() {
session = s
xone_start(s, nil)
}
} else if !present {
closeSession()
}
guard let s = session else { return }
refresh(from: s)
}
private func closeSession() {
if let s = session {
xone_close(s)
session = nil
}
pairingActive = false
controllers = []
errorText = ""
firmwareBuild = ""
}
private func refresh(from s: OpaquePointer) {
state = DongleState(rawValue: Int(xone_state(s))) ?? .idle
pid = xone_pid(s)
chipID = xone_chip_id(s)
macAddress = String(cString: xone_mac_address(s))
firmwareBuild = String(cString: xone_firmware_build(s))
errorText = state == .error ? String(cString: xone_error(s)) : ""
controllers = readControllers(s)
}
private func readControllers(_ s: OpaquePointer) -> [ControllerInput] {
let count = Int(xone_controller_count(s))
var list: [ControllerInput] = []
for i in 0..<count {
var macBuffer = [CChar](repeating: 0, count: 18)
guard xone_controller_mac(s, Int32(i), &macBuffer, 18) == 0 else {
continue
}
var raw = xone_controller_state()
guard xone_controller_get_state(s, Int32(i), &raw) == 0 else {
continue
}
list.append(ControllerInput(
id: String(cString: macBuffer),
buttons: raw.buttons,
guideDown: raw.guide_down,
gipReady: raw.gip_ready,
inputActive: raw.input_active,
triggerLeft: raw.trigger_left,
triggerRight: raw.trigger_right,
stickLeftX: raw.stick_left_x,
stickLeftY: raw.stick_left_y,
stickRightX: raw.stick_right_x,
stickRightY: raw.stick_right_y,
sequence: raw.sequence
))
}
return list
}
}
+4 -304
View File
@@ -1,9 +1,8 @@
// xone_macos macOS app entry point (SwiftUI)
// TODO(phase 5): device discovery (IOKit notifications), pairing UI,
// status display, preferences, menubar icon.
// xone_macos: macOS app entry point (SwiftUI)
// TODO(phase 5): device discovery (IOKit notifications), preferences,
// menubar icon.
import AppKit
import Combine
import SwiftUI
struct XoneApp: App {
@@ -15,6 +14,7 @@ struct XoneApp: App {
}
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// Present as a regular app (foreground window) even when launched
@@ -24,306 +24,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
}
private struct ControllerSnapshot: Identifiable {
let id: String
let buttons: UInt16
let guideDown: Bool
let gipReady: Bool
let inputActive: Bool
let triggerLeft: UInt16
let triggerRight: UInt16
let stickLeftX: Int16
let stickLeftY: Int16
let stickRightX: Int16
let stickRightY: Int16
let sequence: UInt32
}
struct ContentView: View {
@State private var donglePresent = false
@State private var session: OpaquePointer? = nil
// Radio state polled from the background worker each tick; a change here
// is what re-renders the debug section.
@State private var radioState = XONE_STATE_IDLE
// Connected controllers and their latest input snapshot.
@State private var controllers: [ControllerSnapshot] = []
private let timer = Timer.publish(every: 0.02, on: .main, in: .common)
.autoconnect()
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 8) {
Image(systemName: "gamecontroller.fill")
.font(.largeTitle)
Text("xone_macos")
.font(.title)
.bold()
}
statusView
if let session {
debugView(session)
}
Divider()
controllersView
HStack {
Button("Pair Controller") {
if let s = session {
xone_set_pairing(s, true)
}
}
.disabled(radioState != XONE_STATE_READY)
Spacer()
Text("v\(String(cString: xone_version()))")
.foregroundStyle(.secondary)
}
}
.padding(24)
.frame(width: 420)
.frame(minHeight: 320)
.onReceive(timer) { _ in
let present = xone_dongle_present()
if present && session == nil {
// Probe is fast; firmware load + radio init run on a
// background thread so the UI never blocks.
if let s = xone_open() {
session = s
xone_start(s, nil)
radioState = Int(xone_state(s))
}
} else if !present, let s = session {
xone_close(s)
session = nil
} else if let s = session {
radioState = Int(xone_state(s))
refreshControllers(s)
}
donglePresent = present
}
}
private func debugView(_ session: OpaquePointer) -> some View {
VStack(alignment: .leading, spacing: 4) {
Text("Debug")
.font(.headline)
debugRow("PID", String(format: "0x%04X", xone_pid(session)))
debugRow("Chip ID", String(format: "0x%04X", xone_chip_id(session)))
debugRow("MAC", String(cString: xone_mac_address(session)))
debugRow("State", stateText(radioState))
if radioState == XONE_STATE_READY {
debugRow("Firmware", firmwareBuildText(session))
} else if radioState == XONE_STATE_ERROR {
debugRow("Error", String(cString: xone_error(session)))
}
}
}
private func debugRow(_ label: String, _ value: String) -> some View {
HStack {
Text(label)
.foregroundStyle(.secondary)
Spacer()
Text(value)
.font(.system(.body, design: .monospaced))
}
}
private func stateText(_ state: Int) -> String {
switch state {
case XONE_STATE_IDLE: return "idle"
case XONE_STATE_STARTING: return "starting..."
case XONE_STATE_READY: return "ready"
case XONE_STATE_ERROR: return "error"
default: return "unknown"
}
}
private func firmwareBuildText(_ session: OpaquePointer) -> String {
let build = String(cString: xone_firmware_build(session))
return build.isEmpty ? "not loaded" : build
}
// Query the C API for connected controllers and store their MACs.
private func refreshControllers(_ session: OpaquePointer) {
let count = Int(xone_controller_count(session))
var list: [ControllerSnapshot] = []
for i in 0..<count {
var macBuffer = [CChar](repeating: 0, count: 18)
guard xone_controller_mac(session, Int32(i), &macBuffer, 18) == 0 else {
continue
}
var state = xone_controller_state()
guard xone_controller_get_state(session, Int32(i), &state) == 0 else {
continue
}
list.append(ControllerSnapshot(
id: String(cString: macBuffer),
buttons: state.buttons,
guideDown: state.guide_down,
gipReady: state.gip_ready,
inputActive: state.input_active,
triggerLeft: state.trigger_left,
triggerRight: state.trigger_right,
stickLeftX: state.stick_left_x,
stickLeftY: state.stick_left_y,
stickRightX: state.stick_right_x,
stickRightY: state.stick_right_y,
sequence: state.sequence
))
}
controllers = list
}
private var statusView: some View {
HStack(spacing: 8) {
Circle()
.fill(donglePresent ? Color.green : Color.red)
.frame(width: 10, height: 10)
Text(donglePresent ? "Dongle connected" : "No dongle detected")
.font(.headline)
}
}
private var controllersView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Controllers")
.font(.headline)
if controllers.isEmpty {
Text("No controllers connected.")
.foregroundStyle(.secondary)
} else {
ForEach(controllers) { controller in
controllerView(controller)
}
}
}
}
private func controllerView(_ controller: ControllerSnapshot) -> some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: "gamecontroller")
.foregroundStyle(.secondary)
Text(controller.id)
.font(.system(.body, design: .monospaced))
Spacer()
Text(controller.gipReady ? "ready" : "connecting")
.foregroundStyle(controller.gipReady ? .green : .orange)
}
HStack(spacing: 4) {
buttonIndicator("Guide", pressed: controller.guideDown)
buttonIndicator("Start", pressed: isPressed(controller, 0x0004))
buttonIndicator("Select", pressed: isPressed(controller, 0x0008))
buttonIndicator("A", pressed: isPressed(controller, 0x0010))
buttonIndicator("B", pressed: isPressed(controller, 0x0020))
buttonIndicator("X", pressed: isPressed(controller, 0x0040))
buttonIndicator("Y", pressed: isPressed(controller, 0x0080))
}
HStack(spacing: 4) {
buttonIndicator("LB", pressed: isPressed(controller, 0x1000))
buttonIndicator("RB", pressed: isPressed(controller, 0x2000))
buttonIndicator("LS", pressed: isPressed(controller, 0x4000))
buttonIndicator("RS", pressed: isPressed(controller, 0x8000))
buttonIndicator("", pressed: isPressed(controller, 0x0100))
buttonIndicator("", pressed: isPressed(controller, 0x0200))
buttonIndicator("", pressed: isPressed(controller, 0x0400))
buttonIndicator("", pressed: isPressed(controller, 0x0800))
}
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Left stick")
.foregroundStyle(.secondary)
signedAxis("X", controller.stickLeftX)
signedAxis("Y", controller.stickLeftY)
}
VStack(alignment: .leading, spacing: 4) {
Text("Right stick")
.foregroundStyle(.secondary)
signedAxis("X", controller.stickRightX)
signedAxis("Y", controller.stickRightY)
}
}
HStack(spacing: 12) {
triggerAxis("Left trigger", controller.triggerLeft)
triggerAxis("Right trigger", controller.triggerRight)
}
Text(controller.inputActive
? "Input #\(controller.sequence)"
: "Waiting for input")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(10)
.background(Color.secondary.opacity(0.08))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
private func isPressed(_ controller: ControllerSnapshot, _ mask: UInt16) -> Bool {
(controller.buttons & mask) != 0
}
private func buttonIndicator(_ title: String, pressed: Bool) -> some View {
Text(title)
.font(.caption2)
.frame(minWidth: 28, minHeight: 22)
.padding(.horizontal, 2)
.background(pressed ? Color.green : Color.secondary.opacity(0.18))
.foregroundStyle(pressed ? .white : .primary)
.clipShape(RoundedRectangle(cornerRadius: 4))
}
private func signedAxis(_ label: String, _ value: Int16) -> some View {
let normalized = Double(Int(value) + 32768) / 65535.0
return HStack(spacing: 4) {
Text(label)
.font(.caption2)
.frame(width: 12, alignment: .leading)
valueBar(normalized, width: 82)
Text("\(value)")
.font(.caption2.monospacedDigit())
.frame(width: 42, alignment: .trailing)
}
}
private func triggerAxis(_ label: String, _ value: UInt16) -> some View {
HStack(spacing: 4) {
Text(label)
.font(.caption2)
.frame(width: 76, alignment: .leading)
valueBar(Double(value) / 1023.0, width: 70)
Text("\(value)")
.font(.caption2.monospacedDigit())
.frame(width: 32, alignment: .trailing)
}
}
private func valueBar(_ fraction: Double, width: CGFloat) -> some View {
let clamped = min(max(fraction, 0), 1)
return ZStack(alignment: .leading) {
Color.secondary.opacity(0.18)
.frame(width: width, height: 6)
Color.accentColor
.frame(width: width * CGFloat(clamped), height: 6)
}
.frame(width: width, height: 6)
.clipShape(RoundedRectangle(cornerRadius: 3))
}
}
// Entry point: SwiftUI App provides a static main() (top-level code lives in
// main.swift, which CMake's Swift driver expects for executables).
XoneApp.main()