// // VirtualKeyboardView.swift // VoidLink // // Created by True砖家 on 2026/3/25. // Copyright © 2026 True砖家 @ Bilibili. All rights reserved. // import SwiftUI import Combine import UIKit @available(iOS 13.0, *) enum KeyboardType { case win case mac } @available(iOS 13.0, *) enum KeyRole { case normal case fn case keyboardSwitch } @available(iOS 13.0, *) enum KeyVisualStyle { case standard case directionPad } @available(iOS 13.0, *) enum KeyboardSelectionFamily: Hashable { case wasd case arrow } @available(iOS 13.0, *) enum KeyboardSelectionState: Hashable { case none case single(String) case pad } @available(iOS 13.0, *) struct KeyboardWidgetOption: Hashable { let command: String let description: String let highlightedLabels: [String] let family: KeyboardSelectionFamily let selectionState: KeyboardSelectionState } @available(iOS 13.0, *) enum KeyHighlightStyle { case none case single case pad } @available(iOS 13.0, *) public enum VirtualKeyboardMode { case picker case typing case shortcutPicker } @available(iOS 13.0, *) struct Key: Identifiable { let id = UUID() let role: KeyRole let identity: String? let visualStyle: KeyVisualStyle let label: String let width: CGFloat let labelInsets: EdgeInsets let sfSymbolName: String? let winCmdString: String? let macCmdString: String? let winKeyCode: Int16? let macKeyCode: Int16? init( label: String, role: KeyRole = .normal, identity: String? = nil, visualStyle: KeyVisualStyle = .standard, width: CGFloat, labelInsets: EdgeInsets = EdgeInsets(top: 3, leading: 6, bottom: 3, trailing: 6), sfSymbolName: String? = nil, winCmdString: String? = nil, macCmdString: String? = nil, winKeyCode: Int16? = nil, macKeyCode: Int16? = nil ) { self.label = label self.identity = identity self.width = width self.labelInsets = labelInsets self.visualStyle = visualStyle self.sfSymbolName = sfSymbolName self.winCmdString = winCmdString self.macCmdString = macCmdString self.winKeyCode = winKeyCode self.macKeyCode = macKeyCode self.role = role } func cmdString(for keyboardType: KeyboardType) -> String? { switch keyboardType { case .win: return winCmdString case .mac: return macCmdString == nil ? winCmdString : macCmdString } } func keyCode(for keyboardType: KeyboardType) -> Int16? { switch keyboardType { case .win: return winKeyCode == nil ? 0xFF : winKeyCode case .mac: return macKeyCode == nil ? 0xFF : macKeyCode } } } @available(iOS 13.0, *) struct Row: Identifiable { let id = UUID() let keys: [Key] } @available(iOS 13.0, *) struct VirtualKeyboardView: View { let mode: VirtualKeyboardMode var canSelectCommand: ((String) -> Bool)? = nil var isCommandSelected: ((String) -> Bool)? = nil var onCommandSelected: ((String) -> Void)? = nil var onCommandDeselected: ((String) -> Void)? = nil var resetToken: Int = 0 var selectionSyncToken: Int = 0 var externalDeselectionCommand: String? = nil var externalDeselectionToken: Int = 0 @SwiftUI.State private var fnMode = false @SwiftUI.State private var keyboardType: KeyboardType = .win @SwiftUI.State private var showsMouseWidgets: Bool @SwiftUI.State private var highlightedKeyIDs: Set = [] @SwiftUI.State private var directionSingleKeyLabels: Set = [] @SwiftUI.State private var padSelections: Set = [] @SwiftUI.State private var pendingKeyboardOptions: [KeyboardWidgetOption] = [] @SwiftUI.State private var pendingSelectionFamily: KeyboardSelectionFamily? @SwiftUI.State private var showKeyboardWidgetPicker = false @SwiftUI.State private var lastAppliedResetToken: Int = 0 @SwiftUI.State private var lastAppliedSelectionSyncToken: Int = 0 @SwiftUI.State private var lastAppliedExternalDeselectionToken: Int = 0 let keyHeight: CGFloat = 36 let spacing: CGFloat = 6 let unit: CGFloat = 36 // 1u = 正方形边长 static var tappedKeyLabels: [String] = [] static var selectedCmd: String = "" static var lastSelectionDisplayText: String? = nil static var lastSelectionUsesMacLayout = false static var lastSelectionFromMouseWidgets = false static var lastSelectionUsesFnCommandDisplay = false init( mode: VirtualKeyboardMode = .typing, canSelectCommand: ((String) -> Bool)? = nil, isCommandSelected: ((String) -> Bool)? = nil, onCommandSelected: ((String) -> Void)? = nil, onCommandDeselected: ((String) -> Void)? = nil, resetToken: Int = 0, selectionSyncToken: Int = 0, externalDeselectionCommand: String? = nil, externalDeselectionToken: Int = 0 ) { self.mode = mode self.canSelectCommand = canSelectCommand self.isCommandSelected = isCommandSelected self.onCommandSelected = onCommandSelected self.onCommandDeselected = onCommandDeselected self.resetToken = resetToken self.selectionSyncToken = selectionSyncToken self.externalDeselectionCommand = externalDeselectionCommand self.externalDeselectionToken = externalDeselectionToken _showsMouseWidgets = SwiftUI.State(initialValue: mode != .typing) } var body: some View { Group { if usesPickerLayout { GeometryReader { proxy in let isPhone = UIDevice.current.userInterfaceIdiom == .phone let horizontalInset: CGFloat = isPhone ? 6 : 12 let verticalInset: CGFloat = isPhone ? 6 : 12 let availableWidth = max(proxy.size.width - horizontalInset * 2, 1) let availableHeight = max(proxy.size.height - verticalInset * 2, 1) let scale = min( availableWidth / pickerContentWidth, availableHeight / pickerContentHeight, 1 ) ZStack { Color.clear pickerContent .frame(width: pickerContentWidth, height: pickerContentHeight, alignment: .topLeading) .scaleEffect(scale, anchor: .topLeading) .frame( width: pickerContentWidth * scale, height: pickerContentHeight * scale, alignment: .topLeading ) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, horizontalInset) .padding(.vertical, verticalInset) } } else { ScrollView(.horizontal) { HStack(alignment: .top, spacing: 20) { mainBlock navBlock numpadBlock } .padding() } } } .overlay( Group { if showKeyboardWidgetPicker { KeyboardWidgetPickerOverlay( options: pendingKeyboardOptions, onSelect: { option in selectKeyboardWidget(option) }, onReset: { resetCurrentKeyboardWidgetSelection() }, onCancel: { showKeyboardWidgetPicker = false } ) .zIndex(999) } } ) .onReceive(Just(resetToken)) { token in guard token != lastAppliedResetToken else { return } lastAppliedResetToken = token resetAllSelections() } .onReceive(Just(selectionSyncToken)) { token in guard token != lastAppliedSelectionSyncToken else { return } lastAppliedSelectionSyncToken = token synchronizeSelectionHighlights() } .onReceive(Just(externalDeselectionToken)) { token in guard token != lastAppliedExternalDeselectionToken else { return } lastAppliedExternalDeselectionToken = token if let command = externalDeselectionCommand, !command.isEmpty { deselectCommand(command) } } .onAppear { synchronizeSelectionHighlights() } } private var pickerContent: some View { VStack(alignment: .leading, spacing: pickerSectionSpacing) { mainBlock pickerBottomRow } } private var pickerBottomRow: some View { ZStack(alignment: .topLeading) { Color.clear navBlock .frame(width: navBlockWidth, alignment: .leading) .offset(x: equalBottomRowGap) numpadBlock .frame(width: numpadBlockWidth, alignment: .leading) .offset(x: equalBottomRowGap * 2 + navBlockWidth) } .frame(width: mainBlockWidth, height: totalHeight, alignment: .topLeading) } // MARK: - 主键区 var mainBlock: some View { VStack(spacing: spacing) { ForEach(mainRows) { row in HStack(spacing: spacing) { ForEach(row.keys) { key in KeyView( key: key, height: keyHeight, keyboardType: keyboardType, mode: mode, usesDirectionPadBaseStyle: usesDirectionPadBaseStyle, isActive: (key.role == .fn && fnMode) || (key.role == .keyboardSwitch && keyboardType == .mac), highlightStyle: highlightStyle(for: key), action: { handleKeyTap(key) } ) } } } } } @ViewBuilder private func keyboardKeyView( _ key: Key, height: CGFloat? = nil, isActive: Bool = false, customAction: (() -> Void)? = nil ) -> some View { KeyView( key: key, height: height ?? keyHeight, keyboardType: keyboardType, mode: mode, usesDirectionPadBaseStyle: usesDirectionPadBaseStyle, isActive: isActive, highlightStyle: highlightStyle(for: key), action: { if let customAction = customAction { customAction() } else { handleKeyTap(key) } } ) } private func handleKeyTap(_ key: Key) { switch key.role { case .fn: fnMode.toggle() case .keyboardSwitch: keyboardType = keyboardType == .win ? .mac : .win case .normal: if usesPickerSelectionBehavior, let family = selectionFamily(for: key) { handleDirectionKeyTap(key, family: family) } else if usesPickerSelectionBehavior || isTypingToggleKey(key) { toggleRegularKeyHighlight(key) } } } private func isTypingToggleKey(_ key: Key) -> Bool { let normalizedLabel = key.label.lowercased() return normalizedLabel == "tab" || normalizedLabel == "shift" || normalizedLabel == "ctrl" || normalizedLabel == "alt" || normalizedLabel == "control" || normalizedLabel == "opt" } private func toggleRegularKeyHighlight(_ key: Key) { let keyID = regularHighlightID(for: key) if highlightedKeyIDs.contains(keyID) { if mode != .shortcutPicker { let cmd = key.cmdString(for: keyboardType) ?? "" guard !cmd.isEmpty else { return } if canSelectCommand?(cmd) == false { return } VirtualKeyboardView.tappedKeyLabels = [key.label] VirtualKeyboardView.selectedCmd = cmd VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: key) VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac VirtualKeyboardView.lastSelectionFromMouseWidgets = isMouseWidgetKey(key) VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = usesFnCommandDisplay(for: key) onCommandSelected?(cmd) return } highlightedKeyIDs.remove(keyID) if let cmd = key.cmdString(for: keyboardType), !cmd.isEmpty { onCommandDeselected?(cmd) } if VirtualKeyboardView.selectedCmd == key.cmdString(for: keyboardType) { VirtualKeyboardView.selectedCmd = "" } } else { let cmd = key.cmdString(for: keyboardType) ?? "" if !cmd.isEmpty, canSelectCommand?(cmd) == false { return } highlightedKeyIDs.insert(keyID) VirtualKeyboardView.tappedKeyLabels = [key.label] VirtualKeyboardView.selectedCmd = cmd VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: key) VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac VirtualKeyboardView.lastSelectionFromMouseWidgets = isMouseWidgetKey(key) VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = usesFnCommandDisplay(for: key) if !VirtualKeyboardView.selectedCmd.isEmpty { onCommandSelected?(VirtualKeyboardView.selectedCmd) } } } private func handleDirectionKeyTap(_ key: Key, family: KeyboardSelectionFamily) { if padSelections.contains(family) { padSelections.remove(family) onCommandDeselected?(padCommand(for: family)) if VirtualKeyboardView.selectedCmd == padCommand(for: family) { VirtualKeyboardView.selectedCmd = "" } if mode != .shortcutPicker { return } } if directionSingleKeyLabels.contains(key.label) { if mode != .shortcutPicker { let command = key.cmdString(for: keyboardType) ?? key.label if canSelectCommand?(command) == false { return } VirtualKeyboardView.tappedKeyLabels = [key.label] VirtualKeyboardView.selectedCmd = command VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: key) VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac VirtualKeyboardView.lastSelectionFromMouseWidgets = false VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = false onCommandSelected?(command) return } directionSingleKeyLabels.remove(key.label) if let cmd = key.cmdString(for: keyboardType), !cmd.isEmpty { onCommandDeselected?(cmd) } if VirtualKeyboardView.selectedCmd == key.cmdString(for: keyboardType) { VirtualKeyboardView.selectedCmd = "" } return } if mode == .shortcutPicker { let command = key.cmdString(for: keyboardType) ?? key.label if canSelectCommand?(command) == false { return } let option = KeyboardWidgetOption( command: command, description: singleKeyDescription(for: key, family: family), highlightedLabels: [key.label], family: family, selectionState: .single(key.label) ) applySelection(option) VirtualKeyboardView.tappedKeyLabels = [key.label] VirtualKeyboardView.selectedCmd = command VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: option) VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac VirtualKeyboardView.lastSelectionFromMouseWidgets = false VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = false onCommandSelected?(command) return } pendingSelectionFamily = family pendingKeyboardOptions = keyboardOptions(for: key, family: family) VirtualKeyboardView.tappedKeyLabels = [key.label] showKeyboardWidgetPicker = true } private func keyboardOptions(for key: Key, family: KeyboardSelectionFamily) -> [KeyboardWidgetOption] { let singleCommand = key.cmdString(for: keyboardType) ?? key.label let singleDescription = singleKeyDescription(for: key, family: family) switch family { case .wasd: return [ KeyboardWidgetOption( command: singleCommand, description: singleDescription, highlightedLabels: [key.label], family: .wasd, selectionState: .single(key.label) ), KeyboardWidgetOption( command: "WASDPAD", description: LocalizationHelper.localizedString(forKey: "WASD direction pad"), highlightedLabels: ["W", "A", "S", "D"], family: .wasd, selectionState: .pad ) ] case .arrow: return [ KeyboardWidgetOption( command: singleCommand, description: singleDescription, highlightedLabels: [key.label], family: .arrow, selectionState: .single(key.label) ), KeyboardWidgetOption( command: "ARROWPAD", description: LocalizationHelper.localizedString(forKey: "Arrow direction pad"), highlightedLabels: ["↑", "↓", "←", "→"], family: .arrow, selectionState: .pad ) ] } } private func singleKeyDescription(for key: Key, family: KeyboardSelectionFamily) -> String { switch family { case .wasd: return key.label case .arrow: switch key.label { case "↑": return LocalizationHelper.localizedString(forKey: "Up Arrow") case "↓": return LocalizationHelper.localizedString(forKey: "Down Arrow") case "←": return LocalizationHelper.localizedString(forKey: "Left Arrow") case "→": return LocalizationHelper.localizedString(forKey: "Right Arrow") default: return key.label } } } private func poolDisplayText(for key: Key) -> String { key.label } private func poolDisplayText(for option: KeyboardWidgetOption) -> String { switch option.family { case .wasd: if option.selectionState == .pad { return "WASD" } case .arrow: if option.selectionState == .pad { return "Arrows" } } return option.highlightedLabels.first ?? option.command } private func isMouseWidgetKey(_ key: Key) -> Bool { let mouseCommands: Set = [ "MLEFT", "MRIGHT", "MMIDDLE", "MX1", "MX2", "WHEELUP", "WHEELDOWN", "WHEEL", "MOUSEPAD", "TRACKBALL" ] if let command = key.cmdString(for: keyboardType) { return mouseCommands.contains(command) } return false } private func usesFnCommandDisplay(for key: Key) -> Bool { guard fnMode else { return false } let fnRowLabels: Set = [ "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", "App1", "App2" ] return fnRowLabels.contains(key.label) } private func selectKeyboardWidget(_ option: KeyboardWidgetOption) { if canSelectCommand?(option.command) == false { showKeyboardWidgetPicker = false return } applySelection(option) VirtualKeyboardView.tappedKeyLabels = option.highlightedLabels VirtualKeyboardView.selectedCmd = option.command VirtualKeyboardView.lastSelectionDisplayText = poolDisplayText(for: option) VirtualKeyboardView.lastSelectionUsesMacLayout = keyboardType == .mac VirtualKeyboardView.lastSelectionFromMouseWidgets = false VirtualKeyboardView.lastSelectionUsesFnCommandDisplay = false onCommandSelected?(option.command) showKeyboardWidgetPicker = false } private func resetCurrentKeyboardWidgetSelection() { if let family = pendingSelectionFamily { notifyDeselection(for: family) clearSelection(for: family) } VirtualKeyboardView.tappedKeyLabels = [] VirtualKeyboardView.selectedCmd = "" showKeyboardWidgetPicker = false } private func clearSelection(for family: KeyboardSelectionFamily) { directionSingleKeyLabels.subtract(labels(for: family)) padSelections.remove(family) } private func selectionFamily(for key: Key) -> KeyboardSelectionFamily? { if ["W", "A", "S", "D"].contains(key.label) { return .wasd } if ["↑", "↓", "←", "→"].contains(key.label) { return .arrow } return nil } private func labels(for family: KeyboardSelectionFamily) -> [String] { switch family { case .wasd: return ["W", "A", "S", "D"] case .arrow: return ["↑", "↓", "←", "→"] } } private func padCommand(for family: KeyboardSelectionFamily) -> String { switch family { case .wasd: return "WASDPAD" case .arrow: return "ARROWPAD" } } private func applySelection(_ option: KeyboardWidgetOption) { switch option.selectionState { case .single(let label): padSelections.remove(option.family) directionSingleKeyLabels.insert(label) case .pad: directionSingleKeyLabels.subtract(labels(for: option.family)) padSelections.insert(option.family) case .none: clearSelection(for: option.family) } } private func highlightStyle(for key: Key) -> KeyHighlightStyle { if usesPadSelectionHighlight, let family = selectionFamily(for: key), padSelections.contains(family) { return .pad } if highlightedKeyIDs.contains(regularHighlightID(for: key)) || (usesPickerSelectionBehavior && directionSingleKeyLabels.contains(key.label)) { return isPadLikeControl(key) ? .pad : .single } return .none } private func regularHighlightID(for key: Key) -> String { if let identity = key.identity { return identity } let cmd = key.cmdString(for: keyboardType) ?? "NULL" let code = key.keyCode(for: keyboardType) ?? 0xFF return "\(key.label)|\(cmd)|\(code)" } private func isPadLikeControl(_ key: Key) -> Bool { let padCommands: Set = ["WHEEL", "MOUSEPAD", "TRACKBALL", "ABSMOUSEPAD"] if let cmd = key.cmdString(for: keyboardType) { return padCommands.contains(cmd) } return false } var mainRows: [Row] { [ Row(keys: [ Key(label: "Esc", width: unit, winCmdString: "ESC", macCmdString: "ESCAPEMAC", winKeyCode: 0x1B, macKeyCode: 0x35), Key(label: "F1", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "speaker.slash.fill" : "sun.min") : nil, winCmdString: fnMode ? "VOLMUTE" : "F1", macCmdString: fnMode ? "FUNCTION-F1MAC-50MS" : "F1MAC", winKeyCode: fnMode ? 0xAD : 0x70, macKeyCode: 0x7A), Key(label: "F2", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "speaker.wave.1.fill" : "sun.max") : nil, winCmdString: fnMode ? "VOLDOWN" : "F2", macCmdString: fnMode ? "FUNCTION-F2MAC-50MS" : "F2MAC", winKeyCode: fnMode ? 0xAE : 0x71, macKeyCode: 0x78), Key(label: "F3", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "speaker.wave.3.fill" : "rectangle.3.offgrid") : nil, winCmdString: fnMode ? "VOLUP" : "F3", macCmdString: fnMode ? "FUNCTION-F3MAC-50MS" : "F3MAC", winKeyCode: fnMode ? 0xAF : 0x72, macKeyCode: 0x63), Key(label: "F4", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "music.note" : "magnifyingglass") : nil, winCmdString: fnMode ? "MEDIA_SELECT" : "F4", macCmdString: fnMode ? "FUNCTION-F4MAC-50MS" : "F4MAC", winKeyCode: fnMode ? 0xB5 : 0x73, macKeyCode: 0x76), Key(label: "F5", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "backward.fill" : "mic") : nil, winCmdString: fnMode ? "MEDIAPREV" : "F5", macCmdString: fnMode ? "FUNCTION-F5MAC-50MS" : "F5MAC", winKeyCode: fnMode ? 0xB1 : 0x74, macKeyCode: 0x60), Key(label: "F6", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "playpause.fill" : "moon") : nil, winCmdString: fnMode ? "PLAYPAUSE" : "F6", macCmdString: fnMode ? "FUNCTION-F6MAC-50MS" : "F6MAC", winKeyCode: fnMode ? 0xB3 : 0x75, macKeyCode: 0x61), Key(label: "F7", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "forward.fill" : "backward.fill") : nil, winCmdString: fnMode ? "MEDIANEXT" : "F7", macCmdString: fnMode ? "FUNCTION-F7MAC-50MS" : "F7MAC", winKeyCode: fnMode ? 0xB0 : 0x76, macKeyCode: 0x62), Key(label: "F8", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "stop.fill" : "playpause.fill") : nil, winCmdString: fnMode ? "MEDIASTOP" : "F8", macCmdString: fnMode ? "FUNCTION-F8MAC-50MS" : "F8MAC", winKeyCode: fnMode ? 0xB2 : 0x77, macKeyCode: 0x64), Key(label: "F9", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? "envelope.fill" : "forward.fill") : nil, winCmdString: fnMode ? "LAUNCHMAIL" : "F9", macCmdString: fnMode ? "FUNCTION-F9MAC-50MS" : "F9MAC", winKeyCode: fnMode ? 0xB4 : 0x78, macKeyCode: 0x65), Key(label: fnMode ? "App1" : "F10", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? nil : "speaker.slash") : nil, winCmdString: fnMode ? "LAUNCHAPP1" : "F10", macCmdString: fnMode ? "FUNCTION-F10MAC-50MS" : "F10MAC", winKeyCode: fnMode ? 0xB6 : 0x79, macKeyCode: 0x6D), Key(label: fnMode ? "App2" : "F11", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? nil : "speaker.wave.1") : nil, winCmdString: fnMode ? "LAUNCHAPP2" : "F11", macCmdString: fnMode ? "FUNCTION-F11MAC-50MS" : "F11MAC", winKeyCode: fnMode ? 0xB7 : 0x7A, macKeyCode: 0x67), Key(label: "F12", width: unit, sfSymbolName: fnMode ? (keyboardType == .win ? nil : "speaker.wave.3") : nil, winCmdString: "F12", macCmdString: fnMode ? "FUNCTION-F12MAC-50MS" : "F12MAC", winKeyCode: 0x7B, macKeyCode: 0x6F), Key(label: "Null", width: unit, winCmdString: "NULL", macCmdString: "NULL", winKeyCode: 0xFF, macKeyCode: 0xFF), ]), Row(keys: [ Key(label: "` ~", width: unit, winCmdString: "GRAVEACCENT", macCmdString: "GRAVEACCENTMAC", winKeyCode: 0xC0, macKeyCode: 0x32), Key(label: "1 !", width: unit, winCmdString: "1", macCmdString: "1MAC", winKeyCode: 0x31, macKeyCode: 0x53), Key(label: "2 @", width: unit, winCmdString: "2", macCmdString: "2MAC", winKeyCode: 0x32, macKeyCode: 0x54), Key(label: "3 #", width: unit, winCmdString: "3", macCmdString: "3MAC", winKeyCode: 0x33, macKeyCode: 0x55), Key(label: "4 $", width: unit, winCmdString: "4", macCmdString: "4MAC", winKeyCode: 0x34, macKeyCode: 0x56), Key(label: "5 %", width: unit, winCmdString: "5", macCmdString: "5MAC", winKeyCode: 0x35, macKeyCode: 0x57), Key(label: "6 ^", width: unit, winCmdString: "6", macCmdString: "6MAC", winKeyCode: 0x36, macKeyCode: 0x58), Key(label: "7 &", width: unit, winCmdString: "7", macCmdString: "7MAC", winKeyCode: 0x37, macKeyCode: 0x59), Key(label: "8 *", width: unit, winCmdString: "8", macCmdString: "8MAC", winKeyCode: 0x38, macKeyCode: 0x5A), Key(label: "9 (", width: unit, winCmdString: "9", macCmdString: "9MAC", winKeyCode: 0x39, macKeyCode: 0x5B), Key(label: "0 )", width: unit, winCmdString: "0", macCmdString: "0MAC", winKeyCode: 0x30, macKeyCode: 0x52), Key(label: "- _", width: unit, winCmdString: "MINUS", macCmdString: "MINUSMAC", winKeyCode: 0xBD, macKeyCode: 0x1B), Key(label: "= +", width: unit, winCmdString: "EQUALS", macCmdString: "EQUALSMAC", winKeyCode: 0xBB, macKeyCode: 0x18), Key(label: keyboardType == .win ? "Backspace" : "Delete", width: unit * 2, winCmdString: "BACKSPACE", macCmdString: "BACKSPACEMAC", winKeyCode: 0x08, macKeyCode: 0x33) ]), Row(keys: [ Key(label: "Tab", width: unit * 1.5, winCmdString: "TAB", macCmdString: "TABMAC", winKeyCode: 0x09, macKeyCode: 0x30), Key(label: "Q", width: unit, winCmdString: "Q", macCmdString: "QMAC", winKeyCode: 0x51, macKeyCode: 0x0C), Key(label: "W", visualStyle: .directionPad, width: unit, winCmdString: "W", macCmdString: "WMAC", winKeyCode: 0x57, macKeyCode: 0x0D), Key(label: "E", width: unit, winCmdString: "E", macCmdString: "EMAC", winKeyCode: 0x45, macKeyCode: 0x0E), Key(label: "R", width: unit, winCmdString: "R", macCmdString: "RMAC", winKeyCode: 0x52, macKeyCode: 0x0F), Key(label: "T", width: unit, winCmdString: "T", macCmdString: "TMAC", winKeyCode: 0x54, macKeyCode: 0x11), Key(label: "Y", width: unit, winCmdString: "Y", macCmdString: "YMAC", winKeyCode: 0x59, macKeyCode: 0x10), Key(label: "U", width: unit, winCmdString: "U", macCmdString: "UMAC", winKeyCode: 0x55, macKeyCode: 0x20), Key(label: "I", width: unit, winCmdString: "I", macCmdString: "IMAC", winKeyCode: 0x49, macKeyCode: 0x22), Key(label: "O", width: unit, winCmdString: "O", macCmdString: "OMAC", winKeyCode: 0x4F, macKeyCode: 0x1F), Key(label: "P", width: unit, winCmdString: "P", macCmdString: "PMAC", winKeyCode: 0x50, macKeyCode: 0x23), Key(label: "[ {", width: unit, winCmdString: "OPENBRACKET", macCmdString: "OPENBRACKETMAC", winKeyCode: 0xDB, macKeyCode: 0x21), Key(label: "] }", width: unit, winCmdString: "CLOSEBRACKET", macCmdString: "CLOSEBRACKETMAC", winKeyCode: 0xDD, macKeyCode: 0x1E), Key(label: "\\ |", width: unit * 1.5, winCmdString: "BACKSLASH", macCmdString: "BACKSLASHMAC", winKeyCode: 0xDC, macKeyCode: 0x2A) ]), Row(keys: [ Key(label: "Caps", width: unit * 1.75, winCmdString: "CAPSLOCK", macCmdString: "CAPSLOCKMAC", winKeyCode: 0x14, macKeyCode: 0x39), Key(label: "A", visualStyle: .directionPad, width: unit, winCmdString: "A", macCmdString: "AMAC", winKeyCode: 0x41, macKeyCode: 0x00), Key(label: "S", visualStyle: .directionPad, width: unit, winCmdString: "S", macCmdString: "SMAC", winKeyCode: 0x53, macKeyCode: 0x01), Key(label: "D", visualStyle: .directionPad, width: unit, winCmdString: "D", macCmdString: "DMAC", winKeyCode: 0x44, macKeyCode: 0x02), Key(label: "F", width: unit, winCmdString: "F", macCmdString: "FMAC", winKeyCode: 0x46, macKeyCode: 0x03), Key(label: "G", width: unit, winCmdString: "G", macCmdString: "GMAC", winKeyCode: 0x47, macKeyCode: 0x05), Key(label: "H", width: unit, winCmdString: "H", macCmdString: "HMAC", winKeyCode: 0x48, macKeyCode: 0x04), Key(label: "J", width: unit, winCmdString: "J", macCmdString: "JMAC", winKeyCode: 0x4A, macKeyCode: 0x26), Key(label: "K", width: unit, winCmdString: "K", macCmdString: "KMAC", winKeyCode: 0x4B, macKeyCode: 0x28), Key(label: "L", width: unit, winCmdString: "L", macCmdString: "LMAC", winKeyCode: 0x4C, macKeyCode: 0x25), Key(label: "; :", width: unit, winCmdString: "SEMICOLON", macCmdString: "SEMICOLONMAC", winKeyCode: 0xBA, macKeyCode: 0x29), Key(label: "' \"", width: unit, winCmdString: "SINGLEQUOTE", macCmdString: "SINGLEQUOTEMAC", winKeyCode: 0xDE, macKeyCode: 0x27), Key(label: keyboardType == .win ? "Enter" : "Return", identity: "main-enter", width: unit * 2.25, winCmdString: "ENTER", macCmdString: "RETURNMAC", winKeyCode: 0x0D, macKeyCode: 0x24) ]), Row(keys: [ Key(label: "Shift", identity: "left-shift", width: unit * 2.25, winCmdString: "SHIFT", macCmdString: "SHIFTMAC", winKeyCode: 0x10, macKeyCode: 0x38), Key(label: "Z", width: unit, winCmdString: "Z", macCmdString: "ZMAC", winKeyCode: 0x5A, macKeyCode: 0x06), Key(label: "X", width: unit, winCmdString: "X", macCmdString: "XMAC", winKeyCode: 0x58, macKeyCode: 0x07), Key(label: "C", width: unit, winCmdString: "C", macCmdString: "CMAC", winKeyCode: 0x43, macKeyCode: 0x08), Key(label: "V", width: unit, winCmdString: "V", macCmdString: "VMAC", winKeyCode: 0x56, macKeyCode: 0x09), Key(label: "B", width: unit, winCmdString: "B", macCmdString: "BMAC", winKeyCode: 0x42, macKeyCode: 0x0B), Key(label: "N", width: unit, winCmdString: "N", macCmdString: "NMAC", winKeyCode: 0x4E, macKeyCode: 0x2D), Key(label: "M", width: unit, winCmdString: "M", macCmdString: "MMAC", winKeyCode: 0x4D, macKeyCode: 0x2E), Key(label: ", <", width: unit, winCmdString: "COMMA", macCmdString: "COMMAMAC", winKeyCode: 0xBC, macKeyCode: 0x2B), Key(label: ". >", width: unit, winCmdString: "PERIOD", macCmdString: "PERIODMAC", winKeyCode: 0xBE, macKeyCode: 0x2F), Key(label: "/ ?", width: unit, winCmdString: "FORWARDSLASH", macCmdString: "FORWARDSLASHMAC", winKeyCode: 0xBF, macKeyCode: 0x2C), Key(label: "Shift", identity: "right-shift", width: unit * 2.75, winCmdString: "RSHIFT", macCmdString: "SHIFTMAC", winKeyCode: 0xA1, macKeyCode: 0x38) ]), Row(keys: keyboardType == .win ? [ Key(label: keyboardType == .win ? "Win" : "Mac", role: .keyboardSwitch, width: unit * 1.25, winCmdString: "NULL", macCmdString: "NULL", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: "Fn", role: .fn, width: unit * 1.25, winCmdString: "FUNCTION", macCmdString: "FUNCTION", winKeyCode: nil, macKeyCode: 0x3F), Key(label: "Ctrl", width: unit * 1.25, winCmdString: "CTRL", macCmdString: nil, winKeyCode: 0x11, macKeyCode: nil), Key(label: "Win", width: unit * 1.25, sfSymbolName: "square.grid.2x2.fill", winCmdString: "WIN", macCmdString: nil, winKeyCode: 0x5B, macKeyCode: nil), Key(label: "Alt", width: unit * 1.25, winCmdString: "ALT", macCmdString: nil, winKeyCode: 0x12, macKeyCode: nil), Key(label: "Space", width: unit * 6, winCmdString: "SPACE", macCmdString: nil, winKeyCode: 0x20, macKeyCode: nil), Key(label: "Alt", width: unit * 1.25, winCmdString: "RALT", macCmdString: nil, winKeyCode: 0xA5, macKeyCode: nil), Key(label: "Apps", width: unit * 1.25, sfSymbolName: "filemenu.and.selection", winCmdString: "APPS", macCmdString: nil, winKeyCode: 0x5D, macKeyCode: nil), Key(label: "Ctrl", width: unit * 1.25, winCmdString: "RCTRL", macCmdString: nil, winKeyCode: 0xA3, macKeyCode: nil) ]:[ Key(label: keyboardType == .win ? "Win" : "Mac", role: .keyboardSwitch, width: unit * 1.25, winCmdString: "NULL", macCmdString: "NULL", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: "Fn", role: .fn, width: unit * 1.25, winCmdString: "FUNCTION", macCmdString: "FUNCTION", winKeyCode: nil, macKeyCode: 0x3F), Key(label: "Control", width: unit * 1.25, sfSymbolName: "control", winCmdString: nil, macCmdString: "CONTROL", winKeyCode: nil, macKeyCode: 0x3B), Key(label: "Opt", width: unit * 1.25, sfSymbolName: "option", winCmdString: nil, macCmdString: "OPT", winKeyCode: nil, macKeyCode: 0x3A), Key(label: "Cmd", width: unit * 1.25, sfSymbolName: "command", winCmdString: nil, macCmdString: "CMD", winKeyCode: nil, macKeyCode: 0x37), Key(label: "Space", width: unit * 6, winCmdString: nil, macCmdString: "SPACEMAC", winKeyCode: nil, macKeyCode: 0x31), Key(label: "Cmd", width: unit * 1.25, sfSymbolName: "command", winCmdString: nil, macCmdString: "RIGHTCMD", winKeyCode: nil, macKeyCode: 0x36), Key(label: "Opt", width: unit * 1.25, sfSymbolName: "option", winCmdString: nil, macCmdString: "RIGHTOPT", winKeyCode: nil, macKeyCode: 0x3D), Key(label: "Control", width: unit * 1.25, sfSymbolName: "control", winCmdString: nil, macCmdString: "RIGHTCONTROL", winKeyCode: nil, macKeyCode: 0x3E), ]) ] } // MARK: - 导航区 var navBlock: some View { VStack(spacing: spacing) { VStack(spacing: spacing) { HStack(spacing: spacing) { keyboardKeyView(Key(label: keyboardType == .win ? "PrScr" : "F13", width: unit, winCmdString: "PRSCR", macCmdString: "F13MAC", winKeyCode: 0x2C, macKeyCode: 0x69)) keyboardKeyView(Key(label: keyboardType == .win ? "ScrLk" : "F14", width: unit, winCmdString: "SCRLCK", macCmdString: "F14MAC", winKeyCode: 0x91, macKeyCode: 0x6B)) keyboardKeyView(Key(label: keyboardType == .win ? "Pause" : "F15", width: unit, winCmdString: "PAUSE", macCmdString: "F15MAC", winKeyCode: 0x13, macKeyCode: 0x71)) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "Ins", width: unit, sfSymbolName:(keyboardType == .win ? nil : "filemenu.and.selection"), winCmdString: "INSERT", macCmdString: "CONTEXTMAC", winKeyCode: 0x2D, macKeyCode: 0x72)) keyboardKeyView(Key(label: "Home", width: unit, winCmdString: "HOME", macCmdString: "HOMEMAC", winKeyCode: 0x24, macKeyCode: 0x73)) keyboardKeyView(Key(label: "PgUp", width: unit, winCmdString: "PGUP", macCmdString: "PGUPMAC", winKeyCode: 0x21, macKeyCode: 0x74)) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "Del", width: unit, sfSymbolName:(keyboardType == .win ? nil : "delete.right"), winCmdString: "DEL", macCmdString: "DELMAC", winKeyCode: 0x2E, macKeyCode: 0x75)) keyboardKeyView(Key(label: "End", width: unit, winCmdString: "END", macCmdString: "ENDMAC", winKeyCode: 0x23, macKeyCode: 0x77)) keyboardKeyView(Key(label: "PgDn", width: unit, winCmdString: "PGDN", macCmdString: "PGDNMAC", winKeyCode: 0x22, macKeyCode: 0x79)) } } Spacer() VStack(spacing: spacing) { HStack(spacing: spacing) { keyboardKeyView(Key(label: "←", visualStyle: .directionPad, width: unit, winCmdString: "LEFTARR", macCmdString: "LEFTARRMAC", winKeyCode: 0x25, macKeyCode: 0x7B)).opacity(0) keyboardKeyView(Key(label: "↑", visualStyle: .directionPad, width: unit, winCmdString: "UPARR", macCmdString: "UPARRMAC", winKeyCode: 0x26, macKeyCode: 0x7E)) keyboardKeyView(Key(label: "→", visualStyle: .directionPad, width: unit, winCmdString: "RIGHTARR", macCmdString: "RIGHTARRMAC", winKeyCode: 0x27, macKeyCode: 0x7C)).opacity(0) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "←", visualStyle: .directionPad, width: unit, winCmdString: "LEFTARR", macCmdString: "LEFTARRMAC", winKeyCode: 0x25, macKeyCode: 0x7B)) keyboardKeyView(Key(label: "↓", visualStyle: .directionPad, width: unit, winCmdString: "DOWNARR", macCmdString: "DOWNARRMAC", winKeyCode: 0x28, macKeyCode: 0x7D)) keyboardKeyView(Key(label: "→", visualStyle: .directionPad, width: unit, winCmdString: "RIGHTARR", macCmdString: "RIGHTARRMAC", winKeyCode: 0x27, macKeyCode: 0x7C)) } } } .frame(height: totalHeight, alignment: .top) } // MARK: - 小键盘 // MARK: - 小键盘 / 鼠标控件区 var numpadBlock: some View { VStack(spacing: spacing) { if usesPickerLayout { HStack(spacing: spacing) { KeyView( key: Key( label: showsMouseWidgets ? LocalizationHelper.localizedString(forKey: "Mouse Widgets") : LocalizationHelper.localizedString(forKey: "Numpad"), width: unit * 2 + spacing, sfSymbolName: showsMouseWidgets ? "computermouse.fill" : "circle.grid.3x3", winCmdString: "NULL", macCmdString: "NULL", winKeyCode: 0xFF, macKeyCode: 0xFF ), height: keyHeight, keyboardType: keyboardType, mode: mode ) KeyView( key: Key( label: showsMouseWidgets ? LocalizationHelper.localizedString(forKey: "numpad") : LocalizationHelper.localizedString(forKey: "mouse widgets"), width: unit * 2 + spacing, winCmdString: "NULL", macCmdString: "NULL", winKeyCode: 0xFF, macKeyCode: 0xFF ), height: keyHeight, keyboardType: keyboardType, mode: mode, isActive: showsMouseWidgets, action: { showsMouseWidgets.toggle() } ) } } if usesPickerLayout { if showsMouseWidgets { mouseWidgetsBlock } else { numpadKeysBlock } } else { Spacer() numpadKeysBlock } } .frame(height: totalHeight, alignment: .top) } var numpadKeysBlock: some View { VStack(spacing: spacing) { HStack(spacing: spacing) { keyboardKeyView(Key(label: keyboardType == .win ? "Num" : "Clear", width: unit, winCmdString: "NUMLCK", macCmdString: "NUMPADCLEARMAC", winKeyCode: 0x90, macKeyCode: 0x47)) keyboardKeyView(Key(label: "/", width: unit, winCmdString: "DIVIDE", macCmdString: "NUMPADDIVIDEMAC", winKeyCode: 0x6F, macKeyCode: 0x4B)) keyboardKeyView(Key(label: "*", width: unit, winCmdString: "MULTIPLY", macCmdString: "NUMPADMULTIPLYMAC", winKeyCode: 0x6A, macKeyCode: 0x43)) keyboardKeyView(Key(label: "-", width: unit, winCmdString: "SUBTRACT", macCmdString: "NUMPADSUBTRACTMAC", winKeyCode: 0x6D, macKeyCode: 0x4E)) } HStack(alignment: .top, spacing: spacing) { VStack(spacing: spacing) { HStack(spacing: spacing) { keyboardKeyView(Key(label: "7", width: unit, winCmdString: "NUMPAD7", macCmdString: "NUMPAD7MAC", winKeyCode: 0x67, macKeyCode: 0x56)) keyboardKeyView(Key(label: "8", width: unit, winCmdString: "NUMPAD8", macCmdString: "NUMPAD8MAC", winKeyCode: 0x68, macKeyCode: 0x57)) keyboardKeyView(Key(label: "9", width: unit, winCmdString: "NUMPAD9", macCmdString: "NUMPAD9MAC", winKeyCode: 0x69, macKeyCode: 0x58)) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "4", width: unit, winCmdString: "NUMPAD4", macCmdString: "NUMPAD4MAC", winKeyCode: 0x64, macKeyCode: 0x53)) keyboardKeyView(Key(label: "5", width: unit, winCmdString: "NUMPAD5", macCmdString: "NUMPAD5MAC", winKeyCode: 0x65, macKeyCode: 0x54)) keyboardKeyView(Key(label: "6", width: unit, winCmdString: "NUMPAD6", macCmdString: "NUMPAD6MAC", winKeyCode: 0x66, macKeyCode: 0x55)) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "1", width: unit, winCmdString: "NUMPAD1", macCmdString: "NUMPAD1MAC", winKeyCode: 0x61, macKeyCode: 0x50)) keyboardKeyView(Key(label: "2", width: unit, winCmdString: "NUMPAD2", macCmdString: "NUMPAD2MAC", winKeyCode: 0x62, macKeyCode: 0x51)) keyboardKeyView(Key(label: "3", width: unit, winCmdString: "NUMPAD3", macCmdString: "NUMPAD3MAC", winKeyCode: 0x63, macKeyCode: 0x52)) } HStack(spacing: spacing) { keyboardKeyView(Key(label: "0", width: unit * 2 + spacing, winCmdString: "NUMPAD0", macCmdString: "NUMPAD0MAC", winKeyCode: 0x60, macKeyCode: 0x4F)) keyboardKeyView(Key(label: ".", width: unit, winCmdString: "DECIMAL", macCmdString: "NUMPADDECIMALMAC", winKeyCode: 0x6E, macKeyCode: 0x41)) } } VStack(spacing: spacing) { keyboardKeyView( Key(label: "+", width: unit, winCmdString: "ADD", macCmdString: "NUMPADADDMAC", winKeyCode: 0x6B, macKeyCode: 0x45), height: keyHeight * (keyboardType == .win ? 2 : 1) + (keyboardType == .win ? spacing : 0) ) keyboardType == .win ? nil : keyboardKeyView(Key(label: "=", width: unit, winCmdString: nil, macCmdString: "NUMPADEQUALMAC", winKeyCode: nil, macKeyCode: 0x51)) keyboardKeyView( Key(label: "Enter", identity: "numpad-enter", width: unit, winCmdString: "ENTER", macCmdString: "ENTERMAC", winKeyCode: 0x0D, macKeyCode: 0x4C), height: keyHeight * 2 + spacing ) } } } } var mouseWidgetsBlock: some View { VStack(spacing: spacing) { HStack(spacing: spacing) { KeyView( key: Key( label: LocalizationHelper.localizedString(forKey: "Left Button"), width: unit * 2 + spacing, winCmdString: "MLEFT", macCmdString: "MLEFT", winKeyCode: 0xFF, macKeyCode: 0xFF ), height: keyHeight, keyboardType: keyboardType, mode: mode, highlightStyle: highlightStyle(for: Key( label: LocalizationHelper.localizedString(forKey: "Left Button"), width: unit * 2 + spacing, winCmdString: "MLEFT", macCmdString: "MLEFT", winKeyCode: 0xFF, macKeyCode: 0xFF )), action: { handleKeyTap(Key( label: LocalizationHelper.localizedString(forKey: "Left Button"), width: unit * 2 + spacing, winCmdString: "MLEFT", macCmdString: "MLEFT", winKeyCode: 0xFF, macKeyCode: 0xFF )) } ) KeyView( key: Key( label: LocalizationHelper.localizedString(forKey: "Right Button"), width: unit * 2 + spacing, winCmdString: "MRIGHT", macCmdString: "MRIGHT", winKeyCode: 0xFF, macKeyCode: 0xFF ), height: keyHeight, keyboardType: keyboardType, mode: mode, highlightStyle: highlightStyle(for: Key( label: LocalizationHelper.localizedString(forKey: "Right Button"), width: unit * 2 + spacing, winCmdString: "MRIGHT", macCmdString: "MRIGHT", winKeyCode: 0xFF, macKeyCode: 0xFF )), action: { handleKeyTap(Key( label: LocalizationHelper.localizedString(forKey: "Right Button"), width: unit * 2 + spacing, winCmdString: "MRIGHT", macCmdString: "MRIGHT", winKeyCode: 0xFF, macKeyCode: 0xFF )) } ) } HStack(spacing: spacing) { keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Middle Button"), width: unit * 2 + spacing, winCmdString: "MMIDDLE", macCmdString: "MMIDDLE", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "X1"), width: unit, winCmdString: "MX1", macCmdString: "MX1", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "X2"), width: unit, winCmdString: "MX2", macCmdString: "MX2", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) } if mode == .shortcutPicker { HStack(spacing: spacing) { keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Wheel\nUp"), width: unit, winCmdString: "WHEELUP", macCmdString: "WHEELUP", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Wheel\nDown"), width: unit, winCmdString: "WHEELDOWN", macCmdString: "WHEELDOWN", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) } } else { HStack(spacing: spacing) { keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Wheel\nUp"), width: unit, winCmdString: "WHEELUP", macCmdString: "WHEELUP", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Wheel\nDown"), width: unit, winCmdString: "WHEELDOWN", macCmdString: "WHEELDOWN", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "WheelPad"), width: unit * 2 + spacing, winCmdString: "WHEEL", macCmdString: "WHEEL", winKeyCode: 0xFF, macKeyCode: 0xFF ) ) } } if mode != .shortcutPicker { HStack(spacing: spacing) { keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "MousePad"), width: unit * 2 + spacing, winCmdString: "MOUSEPAD", macCmdString: "MOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF ), height: keyHeight * 2 + spacing ) VStack(spacing: spacing) { keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Absolute MousePad"), width: unit * 2 + spacing, winCmdString: "ABSMOUSEPAD", macCmdString: "ABSMOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF ), ) keyboardKeyView( Key( label: LocalizationHelper.localizedString(forKey: "Trackball"), width: unit * 2 + spacing, winCmdString: "TRACKBALL", macCmdString: "TRACKBALL", winKeyCode: 0xFF, macKeyCode: 0xFF ), ) } } } } } var totalHeight: CGFloat { CGFloat(mainRows.count) * keyHeight + CGFloat(mainRows.count - 1) * spacing } private var pickerSectionSpacing: CGFloat { 18 } private var mainBlockWidth: CGFloat { mainRows .map { row in row.keys.reduce(CGFloat(0)) { partialResult, key in partialResult + key.width } + CGFloat(max(row.keys.count - 1, 0)) * spacing } .max() ?? 0 } private var navBlockWidth: CGFloat { unit * 3 + spacing * 2 } private var numpadBlockWidth: CGFloat { unit * 4 + spacing * 3 } private var equalBottomRowGap: CGFloat { max((mainBlockWidth - navBlockWidth - numpadBlockWidth) / 3, 0) } private var pickerContentWidth: CGFloat { mainBlockWidth } private var pickerContentHeight: CGFloat { totalHeight * 2 + pickerSectionSpacing } private func resetAllSelections() { highlightedKeyIDs.removeAll() directionSingleKeyLabels.removeAll() padSelections.removeAll() pendingKeyboardOptions.removeAll() pendingSelectionFamily = nil showKeyboardWidgetPicker = false VirtualKeyboardView.tappedKeyLabels = [] VirtualKeyboardView.selectedCmd = "" } private func synchronizeSelectionHighlights() { highlightedKeyIDs.removeAll() directionSingleKeyLabels.removeAll() padSelections.removeAll() guard let isCommandSelected = isCommandSelected else { return } for family in [KeyboardSelectionFamily.wasd, .arrow] { let command = padCommand(for: family) if isCommandSelected(command) { padSelections.insert(family) } else { for label in labels(for: family) { if let key = key(forLabel: label), let singleCommand = key.cmdString(for: keyboardType), isCommandSelected(singleCommand) { directionSingleKeyLabels.insert(label) } } } } for key in allKeys { guard selectionFamily(for: key) == nil, let command = key.cmdString(for: keyboardType), isCommandSelected(command) else { continue } highlightedKeyIDs.insert(regularHighlightID(for: key)) } } private func deselectCommand(_ command: String) { if command == "WASDPAD" { padSelections.remove(.wasd) } else if command == "ARROWPAD" { padSelections.remove(.arrow) } for family in [KeyboardSelectionFamily.wasd, .arrow] { for label in labels(for: family) { if let key = key(forLabel: label), key.cmdString(for: keyboardType) == command { directionSingleKeyLabels.remove(label) } } } for key in allKeys where key.cmdString(for: keyboardType) == command { highlightedKeyIDs.remove(regularHighlightID(for: key)) } if VirtualKeyboardView.selectedCmd == command { VirtualKeyboardView.selectedCmd = "" } } private func notifyDeselection(for family: KeyboardSelectionFamily) { if padSelections.contains(family) { onCommandDeselected?(padCommand(for: family)) } for label in labels(for: family) where directionSingleKeyLabels.contains(label) { if let key = key(forLabel: label), let cmd = key.cmdString(for: keyboardType), !cmd.isEmpty { onCommandDeselected?(cmd) } } } private func key(forLabel label: String) -> Key? { allKeys.first(where: { $0.label == label }) } private var allKeys: [Key] { mainRows.flatMap(\.keys) + navKeys + numpadKeys + mouseWidgetKeys } private var navKeys: [Key] { [ Key(label: keyboardType == .win ? "PrScr" : "F13", width: unit, winCmdString: "PRSCR", macCmdString: "F13MAC", winKeyCode: 0x2C, macKeyCode: 0x69), Key(label: keyboardType == .win ? "ScrLk" : "F14", width: unit, winCmdString: "SCRLCK", macCmdString: "F14MAC", winKeyCode: 0x91, macKeyCode: 0x6B), Key(label: keyboardType == .win ? "Pause" : "F15", width: unit, winCmdString: "PAUSE", macCmdString: "F15MAC", winKeyCode: 0x13, macKeyCode: 0x71), Key(label: "Ins", width: unit, sfSymbolName:(keyboardType == .win ? nil : "filemenu.and.selection"), winCmdString: "INSERT", macCmdString: "CONTEXTMAC", winKeyCode: 0x2D, macKeyCode: 0x72), Key(label: "Home", width: unit, winCmdString: "HOME", macCmdString: "HOMEMAC", winKeyCode: 0x24, macKeyCode: 0x73), Key(label: "PgUp", width: unit, winCmdString: "PGUP", macCmdString: "PGUPMAC", winKeyCode: 0x21, macKeyCode: 0x74), Key(label: "Del", width: unit, sfSymbolName:(keyboardType == .win ? nil : "delete.right"), winCmdString: "DEL", macCmdString: "DELMAC", winKeyCode: 0x2E, macKeyCode: 0x75), Key(label: "End", width: unit, winCmdString: "END", macCmdString: "ENDMAC", winKeyCode: 0x23, macKeyCode: 0x77), Key(label: "PgDn", width: unit, winCmdString: "PGDN", macCmdString: "PGDNMAC", winKeyCode: 0x22, macKeyCode: 0x79), Key(label: "↑", visualStyle: .directionPad, width: unit, winCmdString: "UPARR", macCmdString: "UPARRMAC", winKeyCode: 0x26, macKeyCode: 0x7E), Key(label: "←", visualStyle: .directionPad, width: unit, winCmdString: "LEFTARR", macCmdString: "LEFTARRMAC", winKeyCode: 0x25, macKeyCode: 0x7B), Key(label: "↓", visualStyle: .directionPad, width: unit, winCmdString: "DOWNARR", macCmdString: "DOWNARRMAC", winKeyCode: 0x28, macKeyCode: 0x7D), Key(label: "→", visualStyle: .directionPad, width: unit, winCmdString: "RIGHTARR", macCmdString: "RIGHTARRMAC", winKeyCode: 0x27, macKeyCode: 0x7C) ] } private var numpadKeys: [Key] { [ Key(label: keyboardType == .win ? "Num" : "Clear", width: unit, winCmdString: "NUMLCK", macCmdString: "NUMPADCLEARMAC", winKeyCode: 0x90, macKeyCode: 0x47), Key(label: "/", width: unit, winCmdString: "DIVIDE", macCmdString: "NUMPADDIVIDEMAC", winKeyCode: 0x6F, macKeyCode: 0x4B), Key(label: "*", width: unit, winCmdString: "MULTIPLY", macCmdString: "NUMPADMULTIPLYMAC", winKeyCode: 0x6A, macKeyCode: 0x43), Key(label: "-", width: unit, winCmdString: "SUBTRACT", macCmdString: "NUMPADSUBTRACTMAC", winKeyCode: 0x6D, macKeyCode: 0x4E), Key(label: "7", width: unit, winCmdString: "NUMPAD7", macCmdString: "NUMPAD7MAC", winKeyCode: 0x67, macKeyCode: 0x56), Key(label: "8", width: unit, winCmdString: "NUMPAD8", macCmdString: "NUMPAD8MAC", winKeyCode: 0x68, macKeyCode: 0x57), Key(label: "9", width: unit, winCmdString: "NUMPAD9", macCmdString: "NUMPAD9MAC", winKeyCode: 0x69, macKeyCode: 0x58), Key(label: "4", width: unit, winCmdString: "NUMPAD4", macCmdString: "NUMPAD4MAC", winKeyCode: 0x64, macKeyCode: 0x53), Key(label: "5", width: unit, winCmdString: "NUMPAD5", macCmdString: "NUMPAD5MAC", winKeyCode: 0x65, macKeyCode: 0x54), Key(label: "6", width: unit, winCmdString: "NUMPAD6", macCmdString: "NUMPAD6MAC", winKeyCode: 0x66, macKeyCode: 0x55), Key(label: "1", width: unit, winCmdString: "NUMPAD1", macCmdString: "NUMPAD1MAC", winKeyCode: 0x61, macKeyCode: 0x50), Key(label: "2", width: unit, winCmdString: "NUMPAD2", macCmdString: "NUMPAD2MAC", winKeyCode: 0x62, macKeyCode: 0x51), Key(label: "3", width: unit, winCmdString: "NUMPAD3", macCmdString: "NUMPAD3MAC", winKeyCode: 0x63, macKeyCode: 0x52), Key(label: "0", width: unit * 2 + spacing, winCmdString: "NUMPAD0", macCmdString: "NUMPAD0MAC", winKeyCode: 0x60, macKeyCode: 0x4F), Key(label: ".", width: unit, winCmdString: "DECIMAL", macCmdString: "NUMPADDECIMALMAC", winKeyCode: 0x6E, macKeyCode: 0x41), Key(label: "+", width: unit, winCmdString: "ADD", macCmdString: "NUMPADADDMAC", winKeyCode: 0x6B, macKeyCode: 0x45), Key(label: "=", width: unit, winCmdString: nil, macCmdString: "NUMPADEQUALMAC", winKeyCode: nil, macKeyCode: 0x51), Key(label: "Enter", identity: "numpad-enter", width: unit, winCmdString: "ENTER", macCmdString: "ENTERMAC", winKeyCode: 0x0D, macKeyCode: 0x4C) ] } private var mouseWidgetKeys: [Key] { var keys = [ Key(label: LocalizationHelper.localizedString(forKey: "Left Button"), width: unit * 2 + spacing, winCmdString: "MLEFT", macCmdString: "MLEFT", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Right Button"), width: unit * 2 + spacing, winCmdString: "MRIGHT", macCmdString: "MRIGHT", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Middle Button"), width: unit * 2 + spacing, winCmdString: "MMIDDLE", macCmdString: "MMIDDLE", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "X1"), width: unit, winCmdString: "MX1", macCmdString: "MX1", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "X2"), width: unit, winCmdString: "MX2", macCmdString: "MX2", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Wheel\nUp"), width: unit, winCmdString: "WHEELDOWN", macCmdString: "WHEELDOWN", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Wheel\nDown"), width: unit, winCmdString: "WHEELUP", macCmdString: "WHEELUP", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "WheelPad"), width: unit * 2 + spacing, winCmdString: "WHEEL", macCmdString: "WHEEL", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "MousePad"), width: unit * 2 + spacing, winCmdString: "MOUSEPAD", macCmdString: "MOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Absolute MousePad"), width: unit * 2 + spacing, winCmdString: "ABSMOUSEPAD", macCmdString: "ABSMOUSEPAD", winKeyCode: 0xFF, macKeyCode: 0xFF), Key(label: LocalizationHelper.localizedString(forKey: "Trackball"), width: unit * 2 + spacing, winCmdString: "TRACKBALL", macCmdString: "TRACKBALL", winKeyCode: 0xFF, macKeyCode: 0xFF) ] if mode == .shortcutPicker { keys.removeAll { key in let command = key.cmdString(for: keyboardType) ?? "" return command == "WHEEL" || command == "MOUSEPAD" || command == "TRACKBALL" || command == "ABSMOUSEPAD" } } return keys } private var usesPickerLayout: Bool { mode != .typing } private var usesPickerSelectionBehavior: Bool { mode == .picker || mode == .shortcutPicker } private var usesPadSelectionHighlight: Bool { mode == .picker } private var usesDirectionPadBaseStyle: Bool { mode == .picker } } @available(iOS 13.0, *) struct KeyView: View { let key: Key let height: CGFloat let keyboardType: KeyboardType let fontSize: CGFloat = 10 var mode: VirtualKeyboardMode = .typing var usesDirectionPadBaseStyle: Bool = true var isActive: Bool = false var highlightStyle: KeyHighlightStyle = .none var action: (() -> Void)? = nil @SwiftUI.State private var isPressed = false var cmdString: String? { key.cmdString(for: keyboardType) } var keyCode: Int16? { key.keyCode(for: keyboardType) } private var keyCornerRadius: CGFloat { 11.0 } private var typingHighlightFill: LinearGradient { LinearGradient( gradient: Gradient(colors: [ Color(red: 0.92, green: 0.945, blue: 0.98), Color(red: 0.76, green: 0.82, blue: 0.90) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) } private var typingHighlightStroke: Color { Color(red: 0.47, green: 0.58, blue: 0.73).opacity(0.62) } private var keyFillGradient: LinearGradient { switch key.visualStyle { case .standard: return LinearGradient( gradient: Gradient(colors: [ Color(red: 0.990, green: 0.988, blue: 0.984), Color(red: 0.920, green: 0.920, blue: 0.910) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) case .directionPad: if usesDirectionPadBaseStyle { return LinearGradient( gradient: Gradient(colors: [ Color(red: 0.88, green: 0.98, blue: 0.96), Color(red: 0.73, green: 0.91, blue: 0.88) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) } return LinearGradient( gradient: Gradient(colors: [ Color(red: 0.990, green: 0.988, blue: 0.984), Color(red: 0.920, green: 0.920, blue: 0.910) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) } } private var keyStrokeColor: Color { switch key.visualStyle { case .standard: return Color(red: 0.66, green: 0.66, blue: 0.64).opacity(0.36) case .directionPad: return usesDirectionPadBaseStyle ? Color(red: 0.18, green: 0.56, blue: 0.52).opacity(0.52) : Color(red: 0.66, green: 0.66, blue: 0.64).opacity(0.36) } } private var pressedFillGradient: LinearGradient { LinearGradient( gradient: Gradient(colors: [ Color(red: 0.84, green: 0.87, blue: 0.91), Color(red: 0.72, green: 0.76, blue: 0.82) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) } private var foregroundColor: Color { if mode == .typing && (highlightStyle != .none || isPressed) { return Color.black.opacity(0.72) } if highlightStyle != .none { if mode == .typing { return Color.black.opacity(0.72) } return Color.black.opacity(0.58) } switch key.visualStyle { case .standard: return Color.primary case .directionPad: return Color.primary } } @ViewBuilder private var keyContent: some View { if let name = key.sfSymbolName, !name.isEmpty { Image(systemName: name) .font(.system(size: fontSize)) .foregroundColor(.black) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { Text(key.label) .font(.system(size: fontSize, weight: .regular, design: .rounded)) .foregroundColor(.black) .lineLimit(key.label.contains("\n") ? 2 : 1) .minimumScaleFactor(0.45) .allowsTightening(true) .multilineTextAlignment(.center) .frame(maxWidth: .infinity, maxHeight: .infinity) } } @ViewBuilder private var keyBackground: some View { if mode == .typing, (highlightStyle != .none || isPressed) { RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .fill(typingHighlightFill) } else if isPressed || isActive { RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .fill(pressedFillGradient) } else if highlightStyle == .pad { RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .fill( LinearGradient( gradient: Gradient(colors: [ Color(red: 0.89, green: 0.90, blue: 1.00), Color(red: 0.64, green: 0.68, blue: 0.96) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) } else if highlightStyle == .single { RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .fill( LinearGradient( gradient: Gradient(colors: [ Color(red: 1.00, green: 0.98, blue: 0.78), Color(red: 0.96, green: 0.80, blue: 0.36) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) } else { RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .fill(keyFillGradient) } } private var borderColor: Color { if mode == .typing, (highlightStyle != .none || isPressed) { return typingHighlightStroke } if highlightStyle == .pad { return Color(red: 0.38, green: 0.42, blue: 0.82).opacity(0.90) } if highlightStyle == .single { return Color.orange.opacity(0.75) } return keyStrokeColor } private var keyShadowColor: Color { if isPressed || isActive { return Color.black.opacity(0.08) } if highlightStyle != .none { return Color.black.opacity(0.08) } return Color.black.opacity(PublicUtils.isIPhone ? 0.13 : 0.15) } var body: some View { keyContent .padding(key.labelInsets) .foregroundColor(foregroundColor) .frame(width: key.width, height: height) .background(keyBackground) .overlay( RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .stroke(Color.white.opacity(isPressed || isActive ? 0.22 : 0.45), lineWidth: 0.76) .padding(1) ) .overlay( RoundedRectangle(cornerRadius: keyCornerRadius, style: .continuous) .stroke(borderColor, lineWidth: highlightStyle == .none ? (key.visualStyle == .standard ? (PublicUtils.isIPhone ? 1.15 : 0.7) : 1.1) : 1.25) ) .shadow(color: keyShadowColor, radius: isPressed || isActive ? 1 : 3, x: 0, y: isPressed || isActive ? 1 : 2) .simultaneousGesture( DragGesture(minimumDistance: 0) .onChanged { _ in if !isPressed { isPressed = true } } .onEnded { _ in isPressed = false action?() // print("cmdString \(String(describing: cmdString)) code \(String(describing: keyCode))") } ) } } @available(iOS 13.0, *) struct KeyboardWidgetPickerOverlay: View { let options: [KeyboardWidgetOption] let onSelect: (KeyboardWidgetOption) -> Void let onReset: () -> Void let onCancel: () -> Void var body: some View { ZStack { Color.clear .contentShape(Rectangle()) .onTapGesture { onCancel() } VStack(spacing: 14) { Text(LocalizationHelper.localizedString(forKey: "Select Control")) .font(.system(size: 18, weight: .bold, design: .rounded)) .foregroundColor(.white) VStack(spacing: 10) { ForEach(options, id: \.self) { option in Button(action: { onSelect(option) }) { HStack(spacing: 12) { Text(option.description) .font(.system(size: 15, weight: .semibold, design: .rounded)) .foregroundColor(.white) Spacer() Text(option.command) .font(.system(size: 12, weight: .bold, design: .rounded)) .foregroundColor(Color.white.opacity(0.72)) } .padding(.horizontal, 14) .frame(height: 44) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.white.opacity(0.10)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.16), lineWidth: 1) ) ) } .buttonStyle(PlainButtonStyle()) } } HStack(spacing: 12) { Button(action: { onCancel() }) { Text(LocalizationHelper.localizedString(forKey: "Cancel")) .font(.system(size: 15, weight: .bold, design: .rounded)) .foregroundColor(Color.white.opacity(0.9)) .frame(maxWidth: .infinity) .frame(height: 42) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.black.opacity(0.16)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.12), lineWidth: 1) ) ) } .buttonStyle(PlainButtonStyle()) Button(action: { onReset() }) { Text(LocalizationHelper.localizedString(forKey: "Reset")) .font(.system(size: 15, weight: .bold, design: .rounded)) .foregroundColor(Color.white.opacity(0.9)) .frame(maxWidth: .infinity) .frame(height: 42) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(Color.black.opacity(0.16)) .overlay( RoundedRectangle(cornerRadius: 12, style: .continuous) .stroke(Color.white.opacity(0.12), lineWidth: 1) ) ) } .buttonStyle(PlainButtonStyle()) } .buttonStyle(PlainButtonStyle()) } .padding(18) .frame(maxWidth: 320) .background( RoundedRectangle(cornerRadius: 22, style: .continuous) .fill( LinearGradient( gradient: Gradient(colors: [ Color(red: 0.15, green: 0.46, blue: 0.50), Color(red: 0.11, green: 0.34, blue: 0.39) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay( RoundedRectangle(cornerRadius: 22, style: .continuous) .stroke(Color.white.opacity(0.18), lineWidth: 1.2) ) ) .shadow(color: Color.black.opacity(0.22), radius: 20, x: 0, y: 10) .padding(.horizontal, 24) } } } @available(iOS 13.0, *) struct VirtualKeyboardView_Previews: PreviewProvider { static var previews: some View { Group { // VirtualKeyboardView(mode: .typing) // .frame(height: 320) VirtualKeyboardView(mode: .picker) .frame(height: 320) .previewDisplayName("Picker") VirtualKeyboardView(mode: .shortcutPicker) .frame(height: 320) .previewDisplayName("Shortcut Picker") } } }