diff --git a/.gitignore b/.gitignore index 1e7bb442..6d05b49c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,9 @@ .plum .deps .HamsterInputSchemas -Resources/SharedSupport/** +Resources/SharedSupport/*.zip *.zip +*.tgz # xcode gitignore @@ -19,3 +20,4 @@ xcuserdata/ *.xcscmblueprint *.xccheckout .history +Frameworks/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0c707c4a..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "Packages/LibrimeKit"] - path = Packages/LibrimeKit - url = https://github.com/imfuxiao/LibrimeKit.git diff --git a/General/AppConstants.swift b/General/AppConstants.swift deleted file mode 100644 index 3ac3bb33..00000000 --- a/General/AppConstants.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import UIKit - -enum AppConstants { - // AppGroup ID - static let appGroupName = "group.dev.fuxiao.app.Hamster" - // iCloud ID - static let iCloudID = "iCloud.dev.fuxiao.app.hamsterapp" - - // keyboard Bundle ID - static let keyboardBundleID = "dev.fuxiao.app.Hamster.HamsterKeyboard" - - // TODO: 系统添加键盘URL - static let addKeyboardPath = "app-settings:root=General&path=Keyboard/KEYBOARDS" - - // 与Squirrel.app保持一致 - // 预先构建的数据目录中 - static let rimeSharedSupportPathName = "SharedSupport" - static let rimeUserPathName = "Rime" - static let inputSchemaZipFile = "SharedSupport.zip" - - // 注意: 此值需要与info.plist中的参数保持一致 - static let appURL = "hamster://dev.fuxiao.app.hamster" - - // 应用日志文件URL - static let logFileURL = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: AppConstants.appGroupName)! - .appendingPathComponent("HamsterApp.log") -} diff --git a/General/File/File+Backup.swift b/General/File/File+Backup.swift deleted file mode 100644 index 3e6c2bfc..00000000 --- a/General/File/File+Backup.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// File+Backup.swift -// Hamster -// -// Created by morse on 7/5/2023. -// - -import Foundation -import Plist -import Yams -import ZIPFoundation - -extension FileManager { - enum BackupError: Error { - case generatorAppSettingsYamlError - } - - static var tempBackupDirectory: URL { - FileManager.default.temporaryDirectory - .appendingPathComponent("HamsterBackup") - } - - static var tempSharedSupportDirectory: URL { - tempBackupDirectory.appendingPathComponent("RIME").appendingPathComponent(AppConstants.rimeSharedSupportPathName) - } - - static var tempUserDataDirectory: URL { - tempBackupDirectory.appendingPathComponent("RIME").appendingPathComponent(AppConstants.rimeUserPathName) - } - - static var tempAppSettingsYaml: URL { - tempBackupDirectory.appendingPathComponent("appSettings.yaml") - } - - static var tempSwipePlist: URL { - tempBackupDirectory.appendingPathComponent("swipe.plist") - } - - static var backupFileNameDateFormat: DateFormatter { - let format = DateFormatter() - format.locale = Locale(identifier: "zh_Hans_SG") - format.dateFormat = "yyyyMMdd-HHmmss" - return format - } - - // 创建备份文件 - func hamsterBackup(appSettings: HamsterAppSettings) throws { - // 创建备份临时文件夹 - try FileManager.createDirectory(override: true, dst: FileManager.tempBackupDirectory) - - // copy 当前输入方案 - try FileManager.copyDirectory(override: true, src: RimeContext.sandboxSharedSupportDirectory, dst: FileManager.tempSharedSupportDirectory) - try FileManager.copyDirectory(override: true, src: RimeContext.sandboxUserDataDirectory, dst: FileManager.tempUserDataDirectory) - - // 生成App配置文件 - let settingYaml = appSettings.yaml() - if settingYaml.isEmpty { - throw BackupError.generatorAppSettingsYamlError - } else { - try settingYaml.write(toFile: FileManager.tempAppSettingsYaml.path, atomically: true, encoding: .utf8) - } - - // 生成上下滑动配置文件 - let fileData = try Plist(appSettings.keyboardSwipeGestureSymbol).toData() - try fileData.write(to: FileManager.tempSwipePlist) - - // 生成zip包至备份目录。 -// let backupURL = appSettings.enableAppleCloud ? FileManager.iCloudBackupsURL : RimeEngine.sandboxBackupDirectory - let backupURL = RimeContext.sandboxBackupDirectory - try FileManager.createDirectory(dst: backupURL) - - let fileName = FileManager.backupFileNameDateFormat.string(from: Date()) - - // 生成zip包 - try zipItem(at: FileManager.tempBackupDirectory, to: backupURL.appendingPathComponent("\(fileName).zip")) - } - - /// 备份文件列表 - func backupFiles(appSettings: HamsterAppSettings) -> [URL] { - let backupDirectoryURL = RimeContext.sandboxBackupDirectory - do { - return try contentsOfDirectory(at: backupDirectoryURL, includingPropertiesForKeys: nil) - .filter { - $0.lastPathComponent.hasSuffix(".zip") - } - } catch { - return [] - } - } - - /// 恢复 - func hamsterRestore(_ backupURL: URL, appSettings: HamsterAppSettings) throws { - // 解压zip - if fileExists(atPath: FileManager.tempBackupDirectory.path) { - try removeItem(at: FileManager.tempBackupDirectory) - } - try unzipItem(at: backupURL, to: FileManager.default.temporaryDirectory) - - // 恢复输入方案 - try FileManager.copyDirectory(override: true, src: FileManager.tempSharedSupportDirectory, dst: RimeContext.sandboxSharedSupportDirectory) - try FileManager.copyDirectory(override: true, src: FileManager.tempUserDataDirectory, dst: RimeContext.sandboxUserDataDirectory) - - // 恢复AppSettings - if let yaml = getStringFromFile(at: FileManager.tempAppSettingsYaml), let node = try Yams.compose(yaml: yaml) { - DispatchQueue.main.async { - appSettings.reset(node: node) - } - } - - // 恢复滑动手势设置 - if let data = contents(atPath: FileManager.tempSwipePlist.path) { - DispatchQueue.main.async { - let plist = Plist(data: data) - appSettings.keyboardSwipeGestureSymbol = plist.strDict - } - } - } - - func deleteBackupFile(_ backupURL: URL) throws { - try FileManager.default.removeItem(at: backupURL) - } -} diff --git a/General/File/File+Crypto.swift b/General/File/File+Crypto.swift deleted file mode 100644 index 95e1254f..00000000 --- a/General/File/File+Crypto.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// File+Crypto.swift -// Hamster -// -// Created by morse on 11/5/2023. -// - -import CommonCrypto -import Foundation -extension FileManager { - func sha256OfFile(atPath path: String) -> String { - guard let fileHandle = FileHandle(forReadingAtPath: path) else { return "" } - defer { fileHandle.closeFile() } - - var context = CC_SHA256_CTX() - CC_SHA256_Init(&context) - - let bufferSize = 1024 * 1024 - while autoreleasepool(invoking: { - let data = fileHandle.readData(ofLength: bufferSize) - if !data.isEmpty { - data.withUnsafeBytes { ptr in - _ = CC_SHA256_Update(&context, ptr.baseAddress, CC_LONG(data.count)) - } - return true - } else { - return false - } - }) {} - - var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - CC_SHA256_Final(&hash, &context) - - return hash.map { String(format: "%02hhx", $0) }.joined() - } -} diff --git a/General/File/File+Zip.swift b/General/File/File+Zip.swift deleted file mode 100644 index 42f4d340..00000000 --- a/General/File/File+Zip.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// File+Zip.swift -// HamsterApp -// -// Created by morse on 28/4/2023. -// - -import Foundation -import ZIPFoundation - -// Zip文件解析异常 -struct ZipParsingError: Error { - let message: String -} - -extension FileManager { - func unzip(_ data: Data, dst: URL) throws -> (Bool, Error?) { - let tempZipURL = temporaryDirectory.appendingPathComponent("temp.zip") - if fileExists(atPath: tempZipURL.path) { - try removeItem(at: tempZipURL) - } - createFile(atPath: tempZipURL.path, contents: data) - return try unzip(tempZipURL, dst: dst) - } - - // 解压至用户数据目录 - // 返回值 - // Bool 处理是否成功 - // Error: 处理失败的Error - func unzip(_ zipURL: URL, dst: URL) throws -> (Bool, Error?) { - var tempURL = zipURL - - // 检测是否为iCloudURL, 需要特殊处理 - if zipURL.path.contains("com~apple~CloudDocs") { - // iCloud中的URL须添加安全访问资源语句,否则会异常:Operation not permitted - // startAccessingSecurityScopedResource与stopAccessingSecurityScopedResource必须成对出现 - if !zipURL.startAccessingSecurityScopedResource() { - throw ZipParsingError(message: "Zip文件读取权限受限") - } - - let tempPath = temporaryDirectory.appendingPathComponent(zipURL.lastPathComponent) - - // 临时文件如果存在需要先删除 - if fileExists(atPath: tempPath.path) { - try removeItem(at: tempPath) - } - - try copyItem(atPath: zipURL.path, toPath: tempPath.path) - - // 停止读取url文件 - zipURL.stopAccessingSecurityScopedResource() - - tempURL = tempPath - } - - // 读取ZIP内容 - guard let archive = Archive(url: tempURL, accessMode: .read) else { - return (false, ZipParsingError(message: "读取Zip文件异常")) - } - - // 查找解压的文件夹里有没有名字包含schema.yaml 的文件 - guard let _ = archive.filter({ $0.path.contains("schema.yaml") }).first else { - return (false, ZipParsingError(message: "Zip文件未包含输入方案文件")) - } - - // 解压前先删除原Rime目录 - try removeItem(at: dst) - try unzipItem(at: tempURL, to: dst) - return (true, nil) - } -} diff --git a/General/File/File+iCloud.swift b/General/File/File+iCloud.swift deleted file mode 100644 index b97b185c..00000000 --- a/General/File/File+iCloud.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// File+iCloud.swift -// Hamster -// -// Created by morse on 2/5/2023. -// - -import Combine -import Foundation - -extension FileManager { - // 应用iCloud文件夹 - // 注意:appendingPathComponent("Documents")是非常重要的一点,如果没有它,你的文件夹将不会显示在iCloud Drive里面。 - static var iCloudDocumentURL: URL? { - if let icloudURL = FileManager.default.url(forUbiquityContainerIdentifier: nil) { - return icloudURL.appendingPathComponent("Documents") - } - return nil - } - - // iCloud中RIME使用文件路径 - static var iCloudRimeURL: URL { - iCloudDocumentURL!.appendingPathComponent("RIME") - } - - // iCloud中 RIME sharedSupport 路径 - static var iCloudSharedSupportURL: URL { - iCloudRimeURL.appendingPathComponent(AppConstants.rimeSharedSupportPathName) - } - - // iCloud中 RIME 方案 userData 路径 - static var iCloudUserDataURL: URL { - iCloudRimeURL.appendingPathComponent(AppConstants.rimeUserPathName) - } - - // iCloud 中 RIME 方案同步路径 - static var iCloudRimeSyncURL: URL { - iCloudRimeURL.appendingPathComponent("sync") - } - - // iCloud 中 软件备份路径 - static var iCloudBackupsURL: URL { - iCloudDocumentURL!.appendingPathComponent("backups") - } -} diff --git a/General/File/File+yaml.swift b/General/File/File+yaml.swift deleted file mode 100644 index 9da38517..00000000 --- a/General/File/File+yaml.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// File+yaml.swift -// HamsterApp -// -// Created by morse on 28/4/2023. -// - -import Foundation -import Yams - -extension FileManager { - // 用来获取*.schema.yaml中的schema_id值 - func getSchemaIds(_ urls: [URL]) -> Set { - var schemaIds: Set = [] - - for url in urls { - if let yamlContent = getStringFromFile(at: url) { - do { - if let yamlFileContent = try Yams.load(yaml: yamlContent) as? [String: Any], - let schemaContent = yamlFileContent["schema"] as? [String: Any], - let schemaId = schemaContent["schema_id"] as? String - { - schemaIds.insert(schemaId) - } - } catch { - Logger.shared.log.error("yaml load error \(error.localizedDescription)") - } - } - } - return schemaIds - } - - func getSyncPath(_ url: URL) -> String? { - guard let yamlContent = getStringFromFile(at: url) else { return nil } - do { - if let yamlFileContent = try Yams.load(yaml: yamlContent) as? [String: Any] { - return yamlFileContent["sync_dir"] as? String - } - } catch { - Logger.shared.log.error("yaml load error \(error.localizedDescription), url:\(url)") - } - return nil - } - - func mergePatchSchemaList(_ yamlURL: URL, schemaIds: [String]) -> String? { - guard let yamlContent = getStringFromFile(at: yamlURL) else { - Logger.shared.log.error("get yaml content is nil. url = \(yamlURL)") - return nil - } - - guard var yamlFileContent = try? Yams.load(yaml: yamlContent) as? [String: Any] else { - Logger.shared.log.error("load yaml error. url = \(yamlURL)") - return nil - } - - var mergeSchemaIds = Set(schemaIds) - if var patch = yamlFileContent["patch"] as? [String: Any] { - if let fileSchemaIds = patch["schema_list"] as? [Any] { - for schemaId in fileSchemaIds { - if let id = schemaId as? String { - if !mergeSchemaIds.contains(id) { - mergeSchemaIds.insert(id) - } - } - } - } - patch["schema_list"] = Array(mergeSchemaIds) - yamlFileContent["patch"] = patch - } else { - yamlFileContent["patch"] = ["schema_list": Array(mergeSchemaIds)] - } - return try? Yams.dump(object: yamlFileContent) - } - - // default.custom.yaml 文件补丁 - // 补丁内容为SchemaList - func patchOfSchemaList(_ schemaIds: [String]) -> String? { - do { - return try Yams.dump(object: ["patch": ["schema_list": schemaIds]]) - } catch { - Logger.shared.log.error(error) - return nil - } - } -} diff --git a/General/File/File.swift b/General/File/File.swift deleted file mode 100644 index 33980c13..00000000 --- a/General/File/File.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// File+FileManager.swift -// HamsterApp -// -// Created by morse on 28/4/2023. -// - -import Foundation - -extension FileManager { - /// 获取制定URL下文件或目录URL - func getFilesAndDirectories(for url: URL) -> [URL] { - do { - return try contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants]) - } catch { - Logger.shared.log.error("Error getting files and directories - \(error.localizedDescription)") - return [] - } - } - - /// 获取指定URL路径下 .schema.yaml 文件URL - func getSchemesFile(for url: URL) -> [URL] { - getFilesAndDirectories(for: url).filter { $0.lastPathComponent.hasSuffix(".schema.yaml") } - } - - /// 获取指定URL的文件内容 - func getStringFromFile(at path: URL) -> String? { - guard let data = FileManager.default.contents(atPath: path.path) else { - return nil - } - return String(data: data, encoding: .utf8) - } - - static func createDirectory(override: Bool = false, dst: URL) throws { - let fm = FileManager.default - if fm.fileExists(atPath: dst.path) { - if override { - try fm.removeItem(atPath: dst.path) - } else { - return - } - } - try fm.createDirectory(at: dst, withIntermediateDirectories: true, attributes: nil) - } - - static func copyDirectory(override: Bool = false, src: URL, dst: URL) throws { - let fm = FileManager.default - if fm.fileExists(atPath: dst.path) { - if override { - try fm.removeItem(atPath: dst.path) - } else { - return - } - } - - if !fm.fileExists(atPath: dst.deletingLastPathComponent().path) { - try fm.createDirectory(at: dst.deletingLastPathComponent(), withIntermediateDirectories: true) - } - try fm.copyItem(at: src, to: dst) - } - - /// 增量复制 - /// 增量是指文件名相同且内容相同的文件会跳过,如果是目录,则会比较目录下的内容 - /// filterRegex: 正则表达式,用来过滤复制的文件,true: 需要过滤 - /// filterMatchBreak: 匹配后是否跳过 true 表示跳过匹配文件, 只拷贝非匹配的文件 false 表示只拷贝匹配文件 - static func incrementalCopy(src: URL, dst: URL, filterRegex: [String] = [], filterMatchBreak: Bool = true, override: Bool = true) throws { - let fm = FileManager.default - // 递归获取全部文件 - guard let srcFiles = fm.enumerator(at: src, includingPropertiesForKeys: [.isDirectoryKey]) else { return } - guard let dstFiles = fm.enumerator(at: dst, includingPropertiesForKeys: [.isDirectoryKey]) else { return } - - let dstFilesMapping = dstFiles.allObjects.compactMap { $0 as? URL }.reduce(into: [String: URL]()) { $0[$1.path.replacingOccurrences(of: dst.path, with: "")] = $1 } - let srcPrefix = src.path - - while let file = srcFiles.nextObject() as? URL { - - // 正则过滤: true 表示正则匹配成功,false 表示没有匹配正则 - let match = !(filterRegex.first(where: { file.path.isMatch(regex: $0) }) ?? "" ).isEmpty - - // 匹配且需要跳过匹配项, 这是过滤的默认行为 - if match && filterMatchBreak { - Logger.shared.log.debug("filter filterRegex: \(filterRegex), filterMatchBreak: \(filterMatchBreak), file: \(file.path)") - continue - } - - // 不匹配且设置了不跳过匹配项,这是反向过滤行为,即只copy匹配过滤项文件 - if !filterRegex.isEmpty && !match && !filterMatchBreak { - Logger.shared.log.debug("filter filterRegex: \(filterRegex), match: \(match), filterMatchBreak: \(filterMatchBreak), file: \(file.path)") - continue - } - - Logger.shared.log.debug("incrementalCopy src: \(src.path) dst: \(dst.path), file: \(file.path)") - - let isDirectory = (try? file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false - let relativePath = file.path.hasPrefix(srcPrefix) ? file.path.replacingOccurrences(of: srcPrefix, with: "") : file.path.replacingOccurrences(of: "/private" + srcPrefix, with: "") - - let dstFile = dstFilesMapping[relativePath] ?? dst.appendingPathComponent(relativePath, isDirectory: isDirectory) - - if fm.fileExists(atPath: dstFile.path) { - // 目录不比较内容 - if isDirectory { - continue - } - - if fm.contentsEqual(atPath: file.path, andPath: dstFile.path) { - continue // 文件已存在, 且内容相同,跳过 - } - - if override { - try fm.removeItem(at: dstFile) - } - } - - if !fm.fileExists(atPath: dstFile.deletingLastPathComponent().path) { - try FileManager.createDirectory(dst: dstFile.deletingLastPathComponent()) - } - - if isDirectory { - try FileManager.createDirectory(dst: dstFile) - continue - } - - Logger.shared.log.debug("incrementalCopy copy file: \(file.path) dst: \(dstFile.path)") - try fm.copyItem(at: file, to: dstFile) - } - } -} - -extension String { - func isMatch(regex: String) -> Bool { - if #available(iOS 16, *) { - Logger.shared.log.debug("isMatch #available(iOS 16, *)") - guard let r = try? Regex(regex) else { return false } - return self.contains(r) - } else { - Logger.shared.log.debug("isMatch use NSRegularExpression") - guard let regex = try? NSRegularExpression(pattern: regex) else { return false } - // 这使用utf16计数,以避免表情符号和类似的问题。 - let range = NSRange(location: 0, length: utf16.count) - return regex.firstMatch(in: self, range: range) != nil - } - } -} diff --git a/General/Lab/Keybaord/Actions/HamsterKeyboardActionHandler.swift b/General/Lab/Keybaord/Actions/HamsterKeyboardActionHandler.swift deleted file mode 100644 index 466f3f0e..00000000 --- a/General/Lab/Keybaord/Actions/HamsterKeyboardActionHandler.swift +++ /dev/null @@ -1,177 +0,0 @@ -import KeyboardKit -import SwiftUI - -class HamsterKeyboardActionHandler: StandardKeyboardActionHandler, ObservableObject { - public weak var hamsterKeyboardController: HamsterKeyboardViewController? - // 全键盘滑动处理 - public var swipeGestureHandler: SwipeGestureHandler - public let appSettings: HamsterAppSettings - - @Published - public var isScrolling = false - - // 键盘滑动处理 - let characterDragAction: (HamsterKeyboardViewController) -> ((KeyboardAction, SwipeDirection, Int, Int) -> Void) = { keyboardController in - weak var ivc = keyboardController - guard let ivc = ivc else { return { _, _, _, _ in } } - - // 滑动配置符号或功能映射 - let actionConfig: [String: String] = ivc.appSettings.keyboardSwipeGestureSymbol - - return { [weak ivc] action, direction, offsetX, _ in - guard let ivc = ivc else { return } - - var actionMappingValue: String? - - // 获取滑动动作的映射值 - switch action { - case .character(let char): - actionMappingValue = actionConfig[char.actionKey(direction)] - case .backspace: - actionMappingValue = actionConfig[.backspaceKeyName.actionKey(direction)] - case .primary: - actionMappingValue = actionConfig[.enterKeyName.actionKey(direction)] - case .space: - if direction.isXAxis && ivc.rimeContext.suggestions.isEmpty { - // 空格左右滑动 - ivc.adjustTextPosition(byCharacterOffset: offsetX) - return - } - actionMappingValue = actionConfig[.spaceKeyName.actionKey(direction)] - case .shift: - actionMappingValue = actionConfig[.shiftKeyName.actionKey(direction)] - case .keyboardType(let type): - if (type == .numeric && ivc.keyboardContext.keyboardType.isAlphabetic) || type.isNumberNineGrid { - actionMappingValue = actionConfig[.numberKeyboardButton.actionKey(direction)] - } - case .custom(let char): // 自定义按钮映射 - actionMappingValue = actionConfig[char.actionKey(direction)] - default: - break - } - - guard let actionMappingValue = actionMappingValue, !actionMappingValue.isEmpty else { - return - } - - Logger.shared.log.debug("sliding action mapping: \(actionMappingValue)") - - // #功能指令处理 - if ivc.functionalInstructionsHandled(actionMappingValue) { - return - } - // 字符处理 - ivc.insertText(actionMappingValue) - } - } - - public init( - inputViewController ivc: HamsterKeyboardViewController, - keyboardContext: KeyboardContext, - keyboardFeedbackHandler: KeyboardFeedbackHandler - ) { - weak var keyboardController = ivc - self.hamsterKeyboardController = keyboardController - self.appSettings = ivc.appSettings - self.swipeGestureHandler = HamsterSwipeGestureHandler( - keyboardContext: keyboardContext, - sensitivityX: .custom(points: appSettings.xSwipeSensitivity), - action: characterDragAction(ivc) - ) - - super.init( - keyboardController: ivc, - keyboardContext: ivc.keyboardContext, - keyboardBehavior: ivc.keyboardBehavior, - keyboardFeedbackHandler: ivc.keyboardFeedbackHandler, - autocompleteContext: ivc.autocompleteContext - ) - } - - override func action(for gesture: KeyboardGesture, on action: KeyboardAction) -> KeyboardAction.GestureAction? { - if let hamsterAction = action.hamsterStanderAction(for: gesture) { - return hamsterAction - } - return nil - } - - override func handle(_ gesture: KeyboardKit.KeyboardGesture, on action: KeyboardKit.KeyboardAction) { - Logger.shared.log.debug("gesture: \(gesture.rawValue), action: \(action), swipeGesture isDragging: \(swipeGestureHandler.isDragging), scrolling: \(isScrolling)") - // fix: press与 langPress 同时触发 - if gesture == .release && swipeGestureHandler.isDragging { - return - } - handle(gesture, on: action, replaced: false) - } - - override func handle(_ gesture: KeyboardGesture, on action: KeyboardAction, replaced: Bool) { - // 反馈触发 - triggerFeedback(for: gesture, on: action) - guard let gestureAction = self.action(for: gesture, on: action) else { return } - // TODO: 这里前后可以添加中英自动加入空格等特性 - gestureAction(keyboardController) - // 这里改变键盘类型: 比如双击, 不能在KeyboardAction+Action那里改 - tryChangeKeyboardType(after: gesture, on: action) - tryRegisterEmoji(after: gesture, on: action) - // TODO: 常用符号 - keyboardController?.performTextContextSync() - } - - /** - Try to change `keyboardType` after a `gesture` has been - performed on the provided `action`. - */ - override func tryChangeKeyboardType(after gesture: KeyboardGesture, on action: KeyboardAction) { - guard keyboardBehavior.shouldSwitchToPreferredKeyboardType(after: gesture, on: action) else { return } - let newType = keyboardBehavior.preferredKeyboardType(after: gesture, on: action) - keyboardContext.keyboardType = newType - } - - override func triggerFeedback(for gesture: KeyboardGesture, on action: KeyboardAction) { - guard shouldTriggerFeedback(for: gesture, on: action) else { return } - keyboardFeedbackHandler.triggerFeedback(for: gesture, on: action) - } - - override func handleDrag(on action: KeyboardAction, from startLocation: CGPoint, to currentLocation: CGPoint) { - Logger.shared.log.debug("handleDrag, action: \(action)") - switch action { - case .space: - // space滑动的的开关判断 - if appSettings.enableSpaceSliding { - swipeGestureHandler.handleDragGesture(action: action, from: startLocation, to: currentLocation) - } - default: - // TODO: 如果上个滑动手势还未结束,在不在进行 - if swipeGestureHandler.isDragging { - return - } - if appSettings.enableKeyboardSwipeGestureSymbol { - swipeGestureHandler.handleDragGesture(action: action, from: startLocation, to: currentLocation) - } - } - } -} - -private extension String { - // 空格名称 - static let spaceKeyName = "space" - // 删除键名称 - static let backspaceKeyName = "backspace" - // 回车键名称 - static let enterKeyName = "enter" - // shift - static let shiftKeyName = "shift" - // 数字键盘切换键 - static let numberKeyboardButton = "123" - - // 获取滑动ActionKey - func actionKey(_ slidingDirection: SwipeDirection) -> String { - var actionKey: String - if slidingDirection.isXAxis { - actionKey = lowercased() + (slidingDirection == .right ? KeyboardConstant.Character.SlideRight : KeyboardConstant.Character.SlideLeft) - } else { - actionKey = lowercased() + (slidingDirection == .down ? KeyboardConstant.Character.SlideDown : KeyboardConstant.Character.SlideUp) - } - return actionKey - } -} diff --git a/General/Lab/Keybaord/Actions/KeyboardAction+Actions.swift b/General/Lab/Keybaord/Actions/KeyboardAction+Actions.swift deleted file mode 100644 index 0b0359f4..00000000 --- a/General/Lab/Keybaord/Actions/KeyboardAction+Actions.swift +++ /dev/null @@ -1,175 +0,0 @@ -import Foundation -import KeyboardKit - -extension KeyboardAction { - var isHamsterInputAction: Bool { - switch self { - case .character: return true - case .characterMargin: return true - case .emoji: return true - case .image: return true - case .space: return true - case .systemImage: return true - case .custom: return true - default: return false - } - } -} - -extension KeyboardAction { -// // 用于注入 HamsterInputViewController 并兼容 KeyboardKit -// typealias HamsterGestureAction = (HamsterKeyboardViewController?) -> GestureAction - - func hamsterStanderAction(for gesture: KeyboardGesture) -> GestureAction? { - switch gesture { - case .doubleTap: return hamsterStandardDoubleTapAction - case .longPress: return hamsterStandardLongPressAction - case .press: return hamsterStandardPressAction - case .release: return hamsterStandardReleaseAction - case .repeatPress: return hamsterStandardRepeatAction - } - } - - /** - The action that by default should be triggered when the - action is double tapped. - */ - var hamsterStandardDoubleTapAction: GestureAction? { nil } - - /** - The action that by default should be triggered when the - action is long pressed. - */ - var hamsterStandardLongPressAction: GestureAction? { - switch self { - case .keyboardType(let type): - if type == .numeric { - return { _ in } - } - return nil - // .space不返回nil, 是因为需要触发反馈 - case .space: return { _ in } - default: return nil - } - } - - /** - The action that by default should be triggered when the - action is pressed. - */ - var hamsterStandardPressAction: GestureAction? { - switch self { - case .backspace: return { $0?.deleteBackward() } -// case .keyboardType(let type): return { $0?.setKeyboardType(type) } - default: return nil - } - } - - /** - The action that by default should be triggered when the - action is released. - */ - var hamsterStandardReleaseAction: GestureAction? { - switch self { - case .character(let char), .characterMargin(let char): return { - guard let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController else { return } - if ivc.keyboardContext.keyboardType.isNumberNineGrid, ivc.appSettings.enableNumberNineGridInputOnScreenMode { - let pairKey = ivc.getPairSymbols(char) - ivc.textDocumentProxy.insertText(pairKey) - _ = ivc.cursorBackOfSymbols(key: pairKey) - if ivc.returnToPrimaryKeyboardOfSymbols(key: char) { - ivc.keyboardContext.keyboardType = .alphabetic(.lowercased) - } - return - } - ivc.insertText(char) - } - case .dismissKeyboard: return { $0?.dismissKeyboard() } - case .emoji(let emoji): return { - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController { - ivc.textDocumentProxy.insertText(emoji.char) - } - } - case .moveCursorBackward: return { $0?.adjustTextPosition(byCharacterOffset: -1) } - case .moveCursorForward: return { $0?.adjustTextPosition(byCharacterOffset: 1) } - case .nextLocale: return { $0?.selectNextLocale() } - case .nextKeyboard: return { $0?.selectNextKeyboard() } - case .primary: return { - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController { - _ = ivc.inputRimeKeyCode(keyCode: XK_Return) - } - } - case .shift(let currentState): - return { - switch currentState { - case .lowercased: $0?.setKeyboardType(.alphabetic(.uppercased)) - case .auto, .capsLocked, .uppercased: $0?.setKeyboardType(.alphabetic(.lowercased)) - } - } - case .space: return { - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController { - _ = ivc.inputRimeKeyCode(keyCode: XK_space) - } - } - case .tab: return { - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController { - _ = ivc.inputRimeKeyCode(keyCode: XK_Tab) - } - } - // TODO: 自定义按键动作处理 - case .custom(let name): return { - let prefix = "#selectIndex:" - if name.hasPrefix(prefix) { - let index = Int(name.dropFirst(prefix.count)) - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController, let index = index { - _ = ivc.selectCandidateIndex(index: index) - if ivc.rimeContext.userInputKey.isEmpty { - ivc.appSettings.keyboardStatus = .normal - } - } - return - } else if name.hasPrefix("#selectSymbol") { - let symbol = String(name.dropFirst("#selectSymbol".count)) - if let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController { - let selectText = ivc.textDocumentProxy.selectedText ?? "" - let pairSymbol = ivc.getPairSymbols(symbol) - ivc.textDocumentProxy.insertText(pairSymbol) - if ivc.cursorBackOfSymbols(key: pairSymbol), !selectText.isEmpty { - ivc.textDocumentProxy.insertText(selectText) - ivc.textDocumentProxy.adjustTextPosition(byCharacterOffset: 1) - } - } - return - } - $0?.insertText(name) - } - case .keyboardType(let type): return { $0?.setKeyboardType(type) } - case .image(_, _, let imageName): - if imageName.isEmpty { - return nil - } - return { - guard let ivc = $0, let ivc = ivc as? HamsterKeyboardViewController else { - return - } - switch imageName { - case KeyboardConstant.ImageName.switchLanguage: - ivc.switchEnglishChinese() - default: return - } - } - default: return nil - } - } - - /** - The action that by default should be triggered when the - action is pressed, and repeated until it is released. - */ - var hamsterStandardRepeatAction: GestureAction? { - switch self { - case .backspace: return hamsterStandardPressAction - default: return nil - } - } -} diff --git a/General/Lab/Keybaord/Actions/KeyboardAction+Buttons.swift b/General/Lab/Keybaord/Actions/KeyboardAction+Buttons.swift deleted file mode 100644 index f8c179fb..00000000 --- a/General/Lab/Keybaord/Actions/KeyboardAction+Buttons.swift +++ /dev/null @@ -1,48 +0,0 @@ -import CoreGraphics -import KeyboardKit -import SwiftUI - -public extension KeyboardAction { - /** - The action's standard button image. - */ - func hamsterButtonImage(for context: KeyboardContext, rimeContext: RimeContext) -> Image? { - switch self { - case .image(_, _, let imageName): - switch imageName { - case KeyboardConstant.ImageName.switchLanguage: - return Image(rimeContext.asciiMode ? - KeyboardConstant.ImageName.englishLanguageImageName : - KeyboardConstant.ImageName.chineseLanguageImageName) - default: - return nil - } - default: return nil - } - } - - /** - The action's standard button text. - */ - func hamsterButtonText(for context: KeyboardContext) -> String? { - switch self { - case .character(let char): return char - case .emoji(let emoji): return emoji.char - case .emojiCategory(let cat): return cat.fallbackDisplayEmoji.char - case .keyboardType(let type): return type.hamsterButtonText(for: context) - case .nextLocale: return context.locale.languageCode?.uppercased() - case .primary(let type): return type.hamsterButtonText(for: context.locale) - case .space: - // TODO: 空格文字显示逻辑放在页面控制 - return nil -// if context.locale.identifier == "zh-Hans" { -// return "空格" -// } -// return KKL10n.space.hamsterText(for: context) - // 自定义按键显示文字 - case .custom(let name): - return KKL10n.hamsterText(forKey: name, locale: context.locale) - default: return nil - } - } -} diff --git a/General/Lab/Keybaord/Actions/KeyboardAction+Return.swift b/General/Lab/Keybaord/Actions/KeyboardAction+Return.swift deleted file mode 100644 index 98cdc87a..00000000 --- a/General/Lab/Keybaord/Actions/KeyboardAction+Return.swift +++ /dev/null @@ -1,28 +0,0 @@ -import KeyboardKit -import SwiftUI - -public extension KeyboardReturnKeyType { - /** - The standard button to text for a certain locale. - */ - func hamsterButtonText(for locale: Locale) -> String? { - switch self { -// case .custom(let title): -// // workaround until https://github.com/KeyboardKit/KeyboardKit/issues/432 resolved -// if title == "send" { -// return KKL10n.hamsterText(forKey: title, locale: locale) -// } -// return title - case .custom(let title): return title - case .done: return KKL10n.done.hamsterText(for: locale) - case .go: return KKL10n.go.hamsterText(for: locale) - case .join: return KKL10n.join.hamsterText(for: locale) - case .newLine: return nil - case .next: return KKL10n.next.hamsterText(for: locale) - case .return: return KKL10n.return.hamsterText(for: locale) - case .ok: return KKL10n.ok.hamsterText(for: locale) - case .search: return KKL10n.search.hamsterText(for: locale) - case .send: return KKL10n.send.hamsterText(for: locale) - } - } -} diff --git a/General/Lab/Keybaord/Appearance/HamsterKeyboardAppearance.swift b/General/Lab/Keybaord/Appearance/HamsterKeyboardAppearance.swift deleted file mode 100644 index d1608a2a..00000000 --- a/General/Lab/Keybaord/Appearance/HamsterKeyboardAppearance.swift +++ /dev/null @@ -1,499 +0,0 @@ -// -// StandardKeyboardAppearance.swift -// KeyboardKit -// -// Created by Daniel Saidi on 2021-01-10. -// Copyright © 2021-2023 Daniel Saidi. All rights reserved. -// - -import Combine -import CoreGraphics -import KeyboardKit -import SwiftUI - -/** - This standard appearance returns styles that replicates the - look of a native system keyboard. - - You can inherit this class and override any open properties - and functions to customize the appearance. For instance, to - change the background color of inpout keys only, you can do - it like this: - - ```swift - class MyAppearance: StandardKeyboardAppearance { - - override func buttonStyle( - for action: KeyboardAction, - isPressed: Bool - ) -> KeyboardButtonStyle { - let style = super.buttonStyle(for: action, isPressed: isPressed) - if !action.isInputAction { return style } - style.backgroundColor = .red - return style - } - } - ``` - - All buttons will be affected if you only return a new style. - Sometimes that is what you want, but most often perhaps not. - */ -open class HamsterKeyboardAppearance: KeyboardAppearance { - /** - Create a standard keyboard appearance intance. - - - Parameters: - - keyboardContext: The keyboard context to use. - */ - public init(keyboardContext: KeyboardContext, rimeContext: RimeContext, appSettings: HamsterAppSettings) { - self.keyboardContext = keyboardContext - self.appSettings = appSettings - self.rimeContext = rimeContext - self.buttonExtendCharacter = appSettings.buttonExtendCharacter - - appSettings.$rimeColorSchema - .receive(on: RunLoop.main) - .sink { [unowned self] _ in - self.hamsterColorSchema = getCurrentColorSchema() - } - .store(in: &cancelable) - } - - public let appSettings: HamsterAppSettings - public let rimeContext: RimeContext - - public var hamsterColorSchema: HamsterColorSchema? - private var cancelable = Set() - - /** - The keyboard context to use. - */ - public let keyboardContext: KeyboardContext - - // 滑动符号映射显示 - public var buttonExtendCharacter: [String: [String]] - - // MARK: - Keyboard - - /** - The background color to apply to the keyboard. - */ - public var backgroundStyle: KeyboardBackgroundStyle { - if let hamsterColorSchema = hamsterColorSchema { - return KeyboardBackgroundStyle(.color(hamsterColorSchema.backColor)) - } - return .standard - } - - /** - The edge insets to apply to the entire keyboard. - */ - open var keyboardEdgeInsets: EdgeInsets { - switch keyboardContext.deviceType { - case .pad: return EdgeInsets(top: 0, leading: 0, bottom: 4, trailing: 0) - case .phone: return EdgeInsets(top: 0, leading: 0, bottom: -2, trailing: 0) - default: return EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) - } - } - - /** - The keyboard layout configuration to use. - */ - open var keyboardLayoutConfiguration: KeyboardLayoutConfiguration { - .standard(for: keyboardContext) - } - - // MARK: - Buttons - - // hamster启用的配色方案 - func getCurrentColorSchema() -> HamsterColorSchema? { - guard appSettings.enableRimeColorSchema else { return nil } - guard !appSettings.rimeColorSchema.isEmpty else { return nil } - guard let colorSchema = appSettings.rimeTotalColorSchemas.first( - where: { $0.schemaName == appSettings.rimeColorSchema } - ) else { return nil } - return HamsterColorSchema(schema: colorSchema) - } - - /** - The button image to use for a certain `action`, if any. - */ - open func buttonImage(for action: KeyboardAction) -> Image? { - if let image = action.hamsterButtonImage(for: keyboardContext, rimeContext: rimeContext) { - return image - } - return action.standardButtonImage(for: keyboardContext) - } - - /** - The scale factor to apply to the button content, if any. - */ - open func buttonImageScaleFactor(for action: KeyboardAction) -> CGFloat { - switch keyboardContext.deviceType { - case .pad: return 1.2 - default: return 1 - } - } - - /** - The button style to use for a certain `action`, given a - certain `isPressed` state. - */ - open func buttonStyle(for action: KeyboardAction, isPressed: Bool) -> KeyboardButtonStyle { - // TODO: 根据 squirrel 颜色方案动态变更 - if let hamsterColorSchema = hamsterColorSchema { - return KeyboardButtonStyle( - backgroundColor: hamsterColorSchema.backColor == .clear ? buttonBackgroundColor(for: action, isPressed: isPressed) : hamsterColorSchema.backColor, - foregroundColor: hamsterColorSchema.candidateTextColor == .clear ? buttonForegroundColor(for: action, isPressed: isPressed) : hamsterColorSchema.candidateTextColor, - font: buttonFont(for: action), - cornerRadius: buttonCornerRadius(for: action), - border: KeyboardButtonBorderStyle(color: hamsterColorSchema.borderColor, size: 1), - shadow: buttonShadowStyle(for: action) - ) - } - return KeyboardButtonStyle( - backgroundColor: buttonBackgroundColor(for: action, isPressed: isPressed), - foregroundColor: buttonForegroundColor(for: action, isPressed: isPressed), - font: buttonFont(for: action), - cornerRadius: buttonCornerRadius(for: action), - border: buttonBorderStyle(for: action), - shadow: buttonShadowStyle(for: action) - ) - } - - /** - The button text to use for a certain `action`, if any. - */ - open func buttonText(for action: KeyboardAction) -> String? { -// action.standardButtonText(for: keyboardContext) - action.hamsterButtonText(for: keyboardContext) - } - - // MARK: - Callouts - - /** - The callout style to apply to action and input callouts. - */ - open var calloutStyle: KeyboardCalloutStyle { -// var style = KeyboardCalloutStyle.standard - var style = KeyboardCalloutStyle.hamsterStandard - let button = buttonStyle(for: .character(""), isPressed: false) - style.buttonCornerRadius = button.cornerRadius ?? 5 - return style - } - - /** - The style to apply when presenting an ``ActionCallout``. - */ - open var actionCalloutStyle: KeyboardActionCalloutStyle { -// var style = KeyboardActionCalloutStyle.standard - var style = KeyboardActionCalloutStyle.hamsterStandard - style.callout = calloutStyle - return style - } - - /** - The style to apply when presenting an ``InputCallout``. - */ - open var inputCalloutStyle: KeyboardInputCalloutStyle { - var style = KeyboardInputCalloutStyle.standard - style.callout = calloutStyle - return style - } - - // MARK: - Autocomplete - - public var autocompleteToolbarStyle: AutocompleteToolbarStyle { -// return .standard - // return .standard - return .init( - item: AutocompleteToolbarItemStyle( - titleFont: .system( - size: CGFloat(appSettings.rimeCandidateTitleFontSize) - ), - titleColor: .primary, - subtitleFont: .system( - size: CGFloat(appSettings.rimeCandidateCommentFontSize) - ) - ) - ) - } - - // MARK: - Overridable Button Style Components - - /** - The button background color to use for a certain action. - */ - open func buttonBackgroundColor(for action: KeyboardAction, isPressed: Bool) -> Color { - let fullOpacity = keyboardContext.hasDarkColorScheme || isPressed - return action.buttonBackgroundColor(for: keyboardContext, isPressed: isPressed) - .opacity(fullOpacity ? 1 : 0.95) - } - - /** - The button border style to use for a certain action. - */ - open func buttonBorderStyle(for action: KeyboardAction) -> KeyboardButtonBorderStyle { - switch action { - case .emoji, .emojiCategory, .none: return .noBorder - default: return .standard - } - } - - /** - The button corner radius to use for a certain action. - */ - open func buttonCornerRadius(for action: KeyboardAction) -> CGFloat { - keyboardLayoutConfiguration.buttonCornerRadius - } - - /** - The button font to use for a certain action. - */ - open func buttonFont(for action: KeyboardAction) -> KeyboardFont { - let size = buttonFontSize(for: action) - let font = KeyboardFont.system(size: size) - guard let weight = buttonFontWeight(for: action) else { return font } - return font.weight(weight) - } - - /** - The button font size to use for a certain action. - */ - open func buttonFontSize(for action: KeyboardAction) -> CGFloat { - if let override = buttonFontSizePadOverride(for: action) { return override } - if buttonImage(for: action) != nil { return 20 } - if let override = buttonFontSizeActionOverride(for: action) { return override } - let text = buttonText(for: action) ?? "" - if action.isHamsterInputAction && text.isLowercased { return 26 } - if action.isSystemAction || action.isPrimaryAction { return 16 } - return 23 - } - - /** - The button font size to force override for some actions. - */ - func buttonFontSizeActionOverride(for action: KeyboardAction) -> CGFloat? { - switch action { - case .keyboardType(let type): return buttonFontSize(for: type) - case .space: return 16 - default: return nil - } - } - - /** - The button font size to force override for iPad devices. - */ - func buttonFontSizePadOverride(for action: KeyboardAction) -> CGFloat? { - guard keyboardContext.deviceType == .pad else { return nil } - let isLandscape = keyboardContext.interfaceOrientation.isLandscape - guard isLandscape else { return nil } - if action.isAlphabeticKeyboardTypeAction || action.isHamsterInputAction { return 22 } - if action.isKeyboardTypeAction(.numeric) || action.isHamsterInputAction { return 22 } - if action.isKeyboardTypeAction(.symbolic) { return 20 } - return nil - } - - /** - The button font size to use for a certain keyboard type. - */ - open func buttonFontSize(for keyboardType: KeyboardType) -> CGFloat { - switch keyboardType { - case .alphabetic: return 15 - case .numeric: return 16 - case .symbolic: return 14 - case .custom: return 16 - default: return 14 - } - } - - /** - The button font weight to use for a certain action. - - You can override this function to customize this single - button style property. - */ - open func buttonFontWeight(for action: KeyboardAction) -> KeyboardFontWeight? { - if isGregorianAlpha { return .regular } - switch action { - case .backspace: return .regular - case .character(let char): return char.isLowercased ? .light : nil - default: return buttonImage(for: action) != nil ? .light : nil - } - } - - /** - The button foreground color to use for a certain action. - - You can override this function to customize this single - button style property. - */ - open func buttonForegroundColor(for action: KeyboardAction, isPressed: Bool) -> Color { - action.buttonForegroundColor(for: keyboardContext, isPressed: isPressed) - } - - /** - The button shadow style to use for a certain action. - - You can override this function to customize this single - button style property. - */ - open func buttonShadowStyle(for action: KeyboardAction) -> KeyboardButtonShadowStyle { - switch action { - case .characterMargin: return .noShadow - case .emoji, .emojiCategory: return .noShadow - case .none: return .noShadow - default: return .standard - } - } -} - -// MARK: - Internal, Testable Extensions - -extension HamsterKeyboardAppearance { - var isGregorianAlpha: Bool { - keyboardContext.keyboardType.isAlphabetic && keyboardContext.locale.matches(.georgian) - } -} - -extension KeyboardAction { - var buttonBackgroundColorForAllStates: Color? { - switch self { - case .none: return .clear - case .characterMargin: return .clearInteractable - case .emoji: return .clearInteractable - case .emojiCategory: return .clearInteractable - default: return nil - } - } - - func buttonBackgroundColor(for context: KeyboardContext, isPressed: Bool = false) -> Color { - if let color = buttonBackgroundColorForAllStates { return color } - return isPressed ? - buttonBackgroundColorForPressedState(for: context) : - buttonBackgroundColorForIdleState(for: context) - } - - /// 空闲状态按钮背景色 - func buttonBackgroundColorForIdleState(for context: KeyboardContext) -> Color { -// if isUppercasedShiftAction { return buttonBackgroundColorForPressedState(for: context) } -// if isSystemAction { return .standardDarkButtonBackground(for: context) } -// if isPrimaryAction { return .blue } -// if isUppercasedShiftAction { return .standardButtonBackground(for: context) } -// return .standardButtonBackground(for: context) - if isUppercasedShiftAction { return buttonBackgroundColorForPressedState(for: context) } - if isSystemAction { return .hamsterStandardDarkButtonBackground(for: context) } - if isPrimaryAction { return .blue } - if isUppercasedShiftAction { return .hamsterStandardButtonBackground(for: context) } - return .hamsterStandardButtonBackground(for: context) - } - - func buttonBackgroundColorForPressedState(for context: KeyboardContext) -> Color { -// if isSystemAction { return context.hasDarkColorScheme ? .standardButtonBackground(for: context) : .white } -// if isPrimaryAction { return context.hasDarkColorScheme ? .standardDarkButtonBackground(for: context) : .white } -// if isUppercasedShiftAction { return .standardDarkButtonBackground(for: context) } -// return .standardDarkButtonBackground(for: context) - if isSystemAction { return context.hasDarkColorScheme ? .hamsterStandardButtonBackground(for: context) : .white } - if isPrimaryAction { return context.hasDarkColorScheme ? .hamsterStandardDarkButtonBackground(for: context) : .white } - if isUppercasedShiftAction { return .hamsterStandardDarkButtonBackground(for: context) } - return .hamsterStandardDarkButtonBackground(for: context) - } - - var buttonForegroundColorForAllStates: Color? { - switch self { - case .none: return .clear - case .characterMargin: return .clearInteractable - default: return nil - } - } - - func buttonForegroundColor(for context: KeyboardContext, isPressed: Bool = false) -> Color { - if let color = buttonForegroundColorForAllStates { return color } - return isPressed ? - buttonForegroundColorForPressedState(for: context) : - buttonForegroundColorForIdleState(for: context) - } - - func buttonForegroundColorForIdleState(for context: KeyboardContext) -> Color { - let standard = Color.standardButtonForeground(for: context) - if isSystemAction { return standard } - if isPrimaryAction { return .white } - return standard - } - - func buttonForegroundColorForPressedState(for context: KeyboardContext) -> Color { - let standard = Color.standardButtonForeground(for: context) - if isSystemAction { return standard } - if isPrimaryAction { return context.hasDarkColorScheme ? .white : standard } - return standard - } - - func hamsterButtonBackgroundColor(for context: KeyboardContext, isPressed: Bool = false) -> Color { - if let color = buttonBackgroundColorForAllStates { return color } - return isPressed ? - buttonBackgroundColorForPressedState(for: context) : - buttonBackgroundColorForIdleState(for: context) - } -} - -extension KeyboardCalloutStyle { - static var hamsterStandard = KeyboardCalloutStyle( - backgroundColor: .standardButtonBackground, - borderColor: Color.black.opacity(0.5), - buttonCornerRadius: 5, - buttonInset: CGSize(width: 5, height: 5), - cornerRadius: 10, - curveSize: CGSize(width: 8, height: 15), - shadowColor: Color.black.opacity(0.1), - shadowRadius: 5, - textColor: .primary - ) -} - -extension KeyboardActionCalloutStyle { - static var hamsterStandard = KeyboardActionCalloutStyle( - callout: KeyboardCalloutStyle.hamsterStandard, - font: .system(size: 12), - maxButtonSize: CGSize(width: 1000, height: 500), - selectedBackgroundColor: .blue, - selectedForegroundColor: .white, - verticalOffset: nil, - verticalTextPadding: 6 - ) -} - -public struct HamsterColorSchema: Identifiable, Equatable, Codable { - init(schema: ColorSchema) { - self.backColor = schema.backColor.bgrColor ?? .clear - self.borderColor = schema.borderColor.bgrColor ?? .clear - self.textColor = schema.textColor.bgrColor ?? .clear - self.hilitedTextColor = schema.hilitedTextColor.bgrColor ?? .clear - self.hilitedBackColor = schema.hilitedBackColor.bgrColor ?? .clear - self.hilitedCandidateTextColor = schema.hilitedCandidateTextColor.bgrColor ?? .clear - self.hilitedCandidateBackColor = schema.hilitedCandidateBackColor.bgrColor ?? .clear - self.hilitedCommentTextColor = schema.hilitedCommentTextColor.bgrColor ?? .clear - self.candidateTextColor = schema.candidateTextColor.bgrColor ?? .clear - self.commentTextColor = schema.commentTextColor.bgrColor ?? .clear - } - - public var id = UUID() - - var backColor: Color // 窗体背景色 back_color - var borderColor: Color // 边框颜色 border_color - - // MARK: 组字区域,对应键盘候选栏的用户输入字码 - - var textColor: Color // 编码行文字颜色 24位色值,16进制,BGR顺序: text_color - var hilitedTextColor: Color // 编码高亮: hilited_text_color - var hilitedBackColor: Color // 编码背景高亮: hilited_back_color - - // 候选栏颜色 - var hilitedCandidateTextColor: Color // 首选文字颜色: hilited_candidate_text_color - var hilitedCandidateBackColor: Color // 首选背景颜色: hilited_candidate_back_color - // hilited_candidate_label_color 首选序号颜色 - var hilitedCommentTextColor: Color // 首选提示字母色: hilited_comment_text_color - - var candidateTextColor: Color // 次选文字色: candidate_text_color - var commentTextColor: Color // 次选提示色: comment_text_color - // label_color 次选序号颜色 -} diff --git a/General/Lab/Keybaord/Behavior/HamsterKeyboardBehavior.swift b/General/Lab/Keybaord/Behavior/HamsterKeyboardBehavior.swift deleted file mode 100644 index 569fd267..00000000 --- a/General/Lab/Keybaord/Behavior/HamsterKeyboardBehavior.swift +++ /dev/null @@ -1,195 +0,0 @@ -import Foundation -import KeyboardKit - -// 代码来源: KeyboardKit/StandardKeyboardBehavior -// 因类方法部分无法重写, 所以不能直接继承 -class HamsterKeyboardBehavior: KeyboardBehavior { - /** - Create a standard keyboard behavior instance. - - - Parameters: - - keyboardContext: The keyboard context to use. - - doubleTapThreshold: The second threshold to detect a tap as a double tap, by default `0.5`. - - endSentenceThreshold: The second threshold during which a sentence can be auto-closed, by default `3.0`. - - repeatGestureTimer: A timer that is responsible for triggering a repeat gesture action, by default ``RepeatGestureTimer/shared``. - */ - public init( - keyboardContext: KeyboardContext, - appSettings: HamsterAppSettings, - rimeContext: RimeContext, - doubleTapThreshold: TimeInterval = 0.5, - endSentenceThreshold: TimeInterval = 3.0, - repeatGestureTimer: RepeatGestureTimer = .shared - ) { - self.keyboardContext = keyboardContext - self.appSettings = appSettings - self.rimeContext = rimeContext - self.doubleTapThreshold = doubleTapThreshold - self.endSentenceThreshold = endSentenceThreshold - self.repeatGestureTimer = repeatGestureTimer - } - - let appSettings: HamsterAppSettings - let rimeContext: RimeContext - - /// The keyboard context to use. - public let keyboardContext: KeyboardContext - - /// The second threshold to detect a tap as a double tap. - public let doubleTapThreshold: TimeInterval - - /// The second threshold during which a sentence can be auto-closed. - public let endSentenceThreshold: TimeInterval - - /// A timer that is responsible for triggering a repeat gesture action. - public let repeatGestureTimer: RepeatGestureTimer - - var lastShiftCheck = Date() - var lastSpaceTap = Date() - - /** - The range that the backspace key should delete when the - key is long pressed. - */ - public var backspaceRange: DeleteBackwardRange { - let duration = repeatGestureTimer.duration ?? 0 - return duration > 3 ? .word : .character - } - - /** - The preferred keyboard type that should be applied when - a certain gesture has been performed on an action. - 当对一个动作执行了某种手势后,应该应用的首选键盘类型。 - */ - public func preferredKeyboardType( - after gesture: KeyboardGesture, - on action: KeyboardAction - ) -> KeyboardType { - if shouldSwitchToCapsLock(after: gesture, on: action) { return .alphabetic(.capsLocked) } - // 检测开启 shift 自动转小写 - if appSettings.enableKeyboardAutomaticallyLowercase && action.isCharacterAction { - if keyboardContext.keyboardType == .alphabetic(.uppercased) { - return .alphabetic(.lowercased) - } - } - switch action { - case .character, .characterMargin: - if keyboardContext.keyboardType == .alphabetic(.capsLocked) { - return keyboardContext.keyboardType - } - return .alphabetic(.lowercased) - default: - return keyboardContext.keyboardType - } - } - - /** - Whether or not to end the currently typed sentence when - a certain gesture has been performed on an action. - */ - open func shouldEndSentence( - after gesture: KeyboardGesture, - on action: KeyboardAction - ) -> Bool { -// #if os(iOS) || os(tvOS) -// guard gesture == .release, action == .space else { return false } -// let proxy = keyboardContext.textDocumentProxy -// let isNewWord = proxy.isCursorAtNewWord -// let isNewSentence = proxy.isCursorAtNewSentence -// let isClosable = (proxy.documentContextBeforeInput ?? "").hasSuffix(" ") -// let isEndingTap = Date().timeIntervalSinceReferenceDate - lastSpaceTap.timeIntervalSinceReferenceDate < endSentenceThreshold -// let shouldClose = isEndingTap && isNewWord && !isNewSentence && isClosable -// lastSpaceTap = Date() -// return shouldClose -// #else -// return false -// #endif - return false - } - - /** - Whether or not to switch to capslock when a gesture has - been performed on an action. - */ - open func shouldSwitchToCapsLock( - after gesture: KeyboardGesture, - on action: KeyboardAction - ) -> Bool { - switch action { - case .shift: return isDoubleShiftTap - default: return false - } - } - - /** - Whether or not to switch to the preferred keyboard type - when a certain gesture has been performed on an action. - - 当对一个Action进行了某种手势后,是否切换到首选键盘类型。 - */ - open func shouldSwitchToPreferredKeyboardType( - after gesture: KeyboardGesture, - on action: KeyboardAction - ) -> Bool { - switch action { - case .keyboardType(let type): - return type.shouldSwitchToPreferredKeyboardType - case .shift: return true - default: - // 检测键盘是否开启自动小写功能 - if keyboardContext.keyboardType.isAlphabetic(.uppercased) && appSettings.enableKeyboardAutomaticallyLowercase { - return true - } - - // 判断是否需要返回主键盘 - if case .character(let char) = action { - return appSettings.returnToPrimaryKeyboardOfSymbols.contains(char) - } - - if case .characterMargin(let char) = action { - return appSettings.returnToPrimaryKeyboardOfSymbols.contains(char) - } - - return false - } - } - - /** - Whether or not to switch to the preferred keyboard type - after the text document proxy text did change. - 在文本发生变化后应切换到首选的键盘类型 - 注意: KeyboardInputViewController会在`textDidChange`函数调用此方法 - */ - public func shouldSwitchToPreferredKeyboardTypeAfterTextDidChange() -> Bool { - return false - } -} - -private extension HamsterKeyboardBehavior { - var isDoubleShiftTap: Bool { - guard keyboardContext.keyboardType.isAlphabetic else { return false } - let date = Date().timeIntervalSinceReferenceDate - let lastDate = lastShiftCheck.timeIntervalSinceReferenceDate - let isDoubleTap = (date - lastDate) < doubleTapThreshold - lastShiftCheck = isDoubleTap ? Date().addingTimeInterval(-1) : Date() - return isDoubleTap - } -} - -private extension KeyboardType { - var shouldSwitchToPreferredKeyboardType: Bool { - switch self { - case .alphabetic(let state): return state.shouldSwitchToPreferredKeyboardType - default: return false - } - } -} - -private extension KeyboardCase { - var shouldSwitchToPreferredKeyboardType: Bool { - switch self { - case .auto: return true - default: return false - } - } -} diff --git a/General/Lab/Keybaord/Callouts/HamsterActionCalloutContext.swift b/General/Lab/Keybaord/Callouts/HamsterActionCalloutContext.swift deleted file mode 100644 index 38d8ddb5..00000000 --- a/General/Lab/Keybaord/Callouts/HamsterActionCalloutContext.swift +++ /dev/null @@ -1,42 +0,0 @@ -import KeyboardKit -import SwiftUI - -class HamsterActionCalloutContext: ActionCalloutContext { - typealias CalloutAction = (KeyboardContext) -> Void - - init(keyboardContext: KeyboardContext, actionHandler: KeyboardActionHandler, actionProvider: CalloutActionProvider) { - self.keyboardContext = keyboardContext - super.init(actionHandler: actionHandler, actionProvider: actionProvider) - } - - var keyboardContext: KeyboardContext - var calloutAction: CalloutAction? - - /** - 手势结束触发一个自定义完成手势 - */ - override func endDragGesture() { - super.endDragGesture() - if let action = actionHandler as? HamsterKeyboardActionHandler { - Logger.shared.log.debug("HamsterActionCalloutContext.endDragGesture()") - action.swipeGestureHandler.endDragGesture() - } - calloutAction?(keyboardContext) - calloutAction = nil - } - - /** - Update the input actions for a certain keyboard action. - 更新某个键盘动作的InputAction - */ -// override func updateInputs(for action: KeyboardAction?, in geo: GeometryProxy, alignment: HorizontalAlignment? = nil) { -// guard let action = action else { return reset() } -// let actions = actionProvider.calloutActions(for: action) -// self.buttonFrame = geo.frame(in: .named(Self.coordinateSpace)) -// self.alignment = alignment ?? getAlignment(for: geo) -// self.actions = isLeading ? actions : actions.reversed() -// self.selectedIndex = startIndex -// guard isActive else { return } -// triggerHapticFeedbackForSelectionChange() -// } -} diff --git a/General/Lab/Keybaord/Callouts/HamsterCalloutActionProvider.swift b/General/Lab/Keybaord/Callouts/HamsterCalloutActionProvider.swift deleted file mode 100644 index 27c3e18f..00000000 --- a/General/Lab/Keybaord/Callouts/HamsterCalloutActionProvider.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// HamsterCalloutActionProvider.swift -// HamsterKeyboard -// -// Created by morse on 14/4/2023. -// - -import Foundation -import KeyboardKit - -class HamsterCalloutActionProvider: CalloutActionProvider { - let keyboardContext: KeyboardContext - let rimeContext: RimeContext - - init(keyboardContext: KeyboardContext, rimeContext: RimeContext) { - self.keyboardContext = keyboardContext - self.rimeContext = rimeContext - } - - func calloutActions(for action: KeyboardKit.KeyboardAction) -> [KeyboardKit.KeyboardAction] { - switch action { - case .keyboardType(let type): - if type == .numeric || type.isNumberNineGrid { - return [ - .character(FunctionalInstructions.selectInputSchema.rawValue), - ] - } - return [] - default: - return [] - } - } -} diff --git a/General/Lab/Keybaord/Callouts/InputCallout.swift b/General/Lab/Keybaord/Callouts/InputCallout.swift deleted file mode 100644 index 04355c5b..00000000 --- a/General/Lab/Keybaord/Callouts/InputCallout.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// InputCallout.swift -// KeyboardKit -// -// Created by Daniel Saidi on 2021-01-06. -// Copyright © 2021-2023 Daniel Saidi. All rights reserved. -// - -import KeyboardKit -import SwiftUI - -/** - This callout can be used to show the currently typed action - above the pressed keyboard button. - */ -public struct HamsterInputCallout: View { - /** - Create an input callout. - - - Parameters: - - calloutContext: The callout context to use. - - keyboardContext: The keyboard context to use. - - style: The style to apply to the view, by default ``KeyboardInputCalloutStyle/standard``. - */ - public init( - calloutContext: InputCalloutContext, - keyboardContext: KeyboardContext, - style: KeyboardInputCalloutStyle = .standard - ) { - self._calloutContext = ObservedObject(wrappedValue: calloutContext) - self._keyboardContext = ObservedObject(wrappedValue: keyboardContext) - self.style = style - } - - @ObservedObject - private var calloutContext: InputCalloutContext - - @ObservedObject - private var keyboardContext: KeyboardContext - - private let style: KeyboardInputCalloutStyle - - static let coordinateSpace = InputCalloutContext.coordinateSpace - - @EnvironmentObject - private var actionHandler: HamsterKeyboardActionHandler - - public var body: some View { - callout - .transition(.opacity) - .opacity(calloutContext.isActive ? 1 : 0) - .keyboardCalloutShadow(style: style.callout) - .position(position) - .allowsHitTesting(false) - } -} - -private extension HamsterInputCallout { - var callout: some View { - VStack(spacing: 0) { - calloutBubble.offset(y: 1) - calloutButton - } - .compositingGroup() - } - - var calloutBubble: some View { - Text(calloutContext.input ?? "") - .font(style.font.font) - .frame(minWidth: calloutSize.width, minHeight: calloutSize.height) - .foregroundColor(style.callout.textColor) - .background(style.callout.backgroundColor) - .cornerRadius(cornerRadius) - } - - var calloutButton: some View { - CalloutButtonArea( - frame: buttonFrame, - style: style.callout - ) - } -} - -// MARK: - Private View Properties - -private extension HamsterInputCallout { - var buttonFrame: CGRect { - calloutContext.buttonFrame.insetBy( - dx: buttonInset.width, - dy: buttonInset.height - ) - } - - var buttonInset: CGSize { - style.callout.buttonInset - } - - var buttonSize: CGSize { - buttonFrame.size - } - - var calloutSize: CGSize { - CGSize( - width: calloutSizeWidth, - height: calloutSizeHeight - ) - } - - var calloutSizeHeight: CGFloat { - let smallSize = buttonSize.height - return shouldEnforceSmallSize ? smallSize : style.calloutSize.height - } - - var calloutSizeWidth: CGFloat { - let minSize = buttonSize.width + 2 * style.callout.curveSize.width + style.callout.cornerRadius - return max(style.calloutSize.width, minSize) - } - - var cornerRadius: CGFloat { - shouldEnforceSmallSize ? style.callout.buttonCornerRadius : style.callout.cornerRadius - } -} - -// MARK: - Private Functionality - -private extension HamsterInputCallout { - var shouldEnforceSmallSize: Bool { - keyboardContext.deviceType == .phone && keyboardContext.interfaceOrientation.isLandscape - } - - var position: CGPoint { - CGPoint(x: positionX, y: positionY) - } - - var positionX: CGFloat { - buttonFrame.origin.x + buttonSize.width/2 - } - - var positionY: CGFloat { - let base = buttonFrame.origin.y + buttonSize.height/2 - calloutSize.height/2 - let isEmoji = calloutContext.action?.isEmojiAction == true - if isEmoji { return base + 5 } - return base - } -} - -// MARK: - Previews - -#if os(iOS) || os(macOS) || os(watchOS) -struct HamsterInputCallout_Previews: PreviewProvider { - struct Preview: View { - var style: KeyboardInputCalloutStyle { - var style = KeyboardInputCalloutStyle.standard - style.callout.backgroundColor = .blue - style.callout.textColor = .white - style.callout.buttonInset = CGSize(width: 3, height: 3) - return style - } - - @StateObject - var context = InputCalloutContext(isEnabled: true) - - func button(for context: InputCalloutContext) -> some View { - GeometryReader { geo in - HamsterGestureButton( - pressAction: { showCallout(for: geo) }, - endAction: context.resetWithDelay, - label: { _ in Color.red.cornerRadius(5) } - ) - } - .frame(width: 15, height: 15) - .padding() - .background(Color.yellow.cornerRadius(6)) - } - - func showCallout(for geo: GeometryProxy) { - context.updateInput(for: .character("a"), in: geo) - } - - var buttonStack: some View { - HStack { - button(for: context) - button(for: context) - button(for: context) - } - } - - var body: some View { - VStack { - buttonStack - buttonStack - Button("Reset") { - context.reset() - } - } - .keyboardInputCallout( - calloutContext: context, - keyboardContext: .preview - ) - } - } - - static var previews: some View { - Preview() - } -} -#endif diff --git a/General/Lab/Keybaord/Callouts/View+Callouts.swift b/General/Lab/Keybaord/Callouts/View+Callouts.swift deleted file mode 100644 index e3152942..00000000 --- a/General/Lab/Keybaord/Callouts/View+Callouts.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// View+InputCallout.swift -// KeyboardKit -// -// Created by Daniel Saidi on 2021-01-06. -// Copyright © 2021-2023 Daniel Saidi. All rights reserved. -// - -import KeyboardKit -import SwiftUI - -public extension View { - /** - Apply a keyboard action callout to the view. - - - Parameters: - - calloutContext: The callout context to use. - - keyboardContext: The keyboard context to use. - - style: The style to apply to the view, by default ``KeyboardActionCalloutStyle/standard``. - - emojiKeyboardStyle: The emoji keyboard style to use, by default ``EmojiKeyboardStyle/standardPhonePortrait``. - */ - func hamsterKeyboardActionCallout( - calloutContext: ActionCalloutContext, - keyboardContext: KeyboardContext, - style: KeyboardActionCalloutStyle = .standard, - emojiKeyboardStyle: EmojiKeyboardStyle = .standardPhonePortrait - ) -> some View { - self.overlay( - ActionCallout( - calloutContext: calloutContext, - keyboardContext: keyboardContext, - style: style, - emojiKeyboardStyle: emojiKeyboardStyle - ) - ) - .coordinateSpace(name: ActionCalloutContext.coordinateSpace) - } - - /** - Apply a keyboard input callout to the view. - - - Parameters: - - calloutContext: The callout context to use. - - keyboardContext: The keyboard context to use. - - style: The style to apply, by default ``KeyboardInputCalloutStyle/standard``. - */ - func hamsterKeyboardInputCallout( - calloutContext: InputCalloutContext, - keyboardContext: KeyboardContext, - style: KeyboardInputCalloutStyle = .standard - ) -> some View { - self.overlay( - HamsterInputCallout( - calloutContext: calloutContext, - keyboardContext: keyboardContext, - style: style - ) - ) - .coordinateSpace(name: InputCalloutContext.coordinateSpace) - } -} diff --git a/General/Lab/Keybaord/Colors/KeyboardColor.swift b/General/Lab/Keybaord/Colors/KeyboardColor.swift deleted file mode 100644 index a690a1dc..00000000 --- a/General/Lab/Keybaord/Colors/KeyboardColor.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// KeyboardColor.swift -// KeyboardKit -// -// Created by Daniel Saidi on 2021-04-13. -// Copyright © 2021-2023 Daniel Saidi. All rights reserved. -// - -import SwiftUI - -/** - This enum defines raw, keyboard-specific asset-based colors. - - Although you can use this type directly, you should instead - use the ``KeyboardColorReader`` protocol, to get extensions - that build on these colors. `Color` already implements this - protocol, so you can use it directly. - */ - -public enum HamsterKeyboardColor: String, CaseIterable, Identifiable { - case standardButtonBackground - case standardButtonBackgroundForColorSchemeBug - case standardButtonBackgroundForDarkAppearance - case standardButtonForeground - case standardButtonForegroundForDarkAppearance - case standardButtonShadow - case standardDarkButtonBackground - case standardDarkButtonBackgroundForColorSchemeBug - case standardDarkButtonBackgroundForDarkAppearance - case standardKeyboardBackground - case standardKeyboardBackgroundForDarkAppearance -} - -public extension HamsterKeyboardColor { - /** - The color's unique identifier. - */ - var id: String { rawValue } - - /** - The color value. - */ - var color: Color { - Color(resourceName) - } - - /** - The color asset name in the bundle asset catalog. - */ - var resourceName: String { rawValue } -} - -struct HamsterKeyboardColor_Previews: PreviewProvider { - static func preview(for color: HamsterKeyboardColor) -> some View { - VStack(alignment: .leading) { - Text(color.resourceName).font(.footnote) - HStack(spacing: 0) { - color.color - color.color.colorScheme(.dark) - } - .frame(height: 100) - .cornerRadius(10) - } - } - - static var previews: some View { - ScrollView { - VStack { - ForEach(HamsterKeyboardColor.allCases) { - preview(for: $0) - } - }.padding() - }.background(Color.black.opacity(0.1).edgesIgnoringSafeArea(.all)) - } -} diff --git a/General/Lab/Keybaord/Colors/KeyboardColorReader.swift b/General/Lab/Keybaord/Colors/KeyboardColorReader.swift deleted file mode 100644 index 5e1a20b0..00000000 --- a/General/Lab/Keybaord/Colors/KeyboardColorReader.swift +++ /dev/null @@ -1,225 +0,0 @@ -// -// Color+Keyboard.swift -// KeyboardKit -// -// Created by Daniel Saidi on 2021-01-20. -// Copyright © 2021-2023 Daniel Saidi. All rights reserved. -// - -import KeyboardKit -import SwiftUI - -/** - This protocol can be implemented by any type that should be - able to access keyboard-specific colors. - - This protocol is implemented by `Color`. This means that it - is possible to use e.g. `Color.standardButtonBackground` to - get the standard button background. - - The context-based color functions may look strange, but the - reason for having them this way is that iOS gets an invalid - color scheme when editing a text field with dark appearance. - This causes iOS to set the extension's color scheme to dark - even if the system color scheme is light. - - To work around this, some colors have a temporary color set - with a `ForColorSchemeBug` suffix that are semi-transparent - white with an opacity that makes them look ok in both light - and dark mode. - - Issue report (also reported to Apple in Feedback Assistant): - https://github.com/danielsaidi/KeyboardKit/issues/305 - */ - -public extension KeyboardColorReader { - /** - The standard background color of light keyboard buttons. - */ - static var hamsterStandardButtonBackground: Color { - color(for: .standardButtonBackground) - } - - /** - The standard background color of light keyboard buttons - when accounting for the iOS dark mode bug. - */ - static var hamsterStandardButtonBackgroundForColorSchemeBug: Color { - color(for: .standardButtonBackgroundForColorSchemeBug) - } - - /** - The standard background color of light keyboard buttons - in dark keyboard appearance. - */ - static var hamsterStandardButtonBackgroundForDarkAppearance: Color { - color(for: .standardButtonBackgroundForDarkAppearance) - } - - /** - The standard foreground color of light keyboard buttons. - */ - static var hamsterStandardButtonForeground: Color { - color(for: .standardButtonForeground) - } - - /** - The standard foreground color of light keyboard buttons - in dark keyboard appearance. - */ - static var hamsterStandardButtonForegroundForDarkAppearance: Color { - color(for: .standardButtonForegroundForDarkAppearance) - } - - /** - The standard shadow color of keyboard buttons. - */ - static var hamsterStandardButtonShadow: Color { - color(for: .standardButtonShadow) - } - - /** - The standard background color of a dark keyboard button. - */ - static var hamsterStandardDarkButtonBackground: Color { -// color(for: .standardDarkButtonBackground) - Color(UIColor(red: 0, green: 0, blue: 0, alpha: 0.15)) - } - - /** - The standard background color of a dark keyboard button - when accounting for the iOS dark mode bug. - */ - static var hamsterStandardDarkButtonBackgroundForColorSchemeBug: Color { - color(for: .standardDarkButtonBackgroundForColorSchemeBug) - } - - /** - The standard background color of a dark keyboard button - in dark keyboard appearance. - */ - static var hamsterStandardDarkButtonBackgroundForDarkAppearance: Color { - color(for: .standardDarkButtonBackgroundForDarkAppearance) - } - - /** - The standard foreground color of a dark keyboard button. - */ - static var hamsterStandardDarkButtonForeground: Color { - color(for: .standardButtonForeground) - } - - /** - The standard foreground color of a dark keyboard button - in dark keyboard appearance. - */ - static var hamsterStandardDarkButtonForegroundForDarkAppearance: Color { - color(for: .standardButtonForegroundForDarkAppearance) - } - - /** - The standard keyboard background color. - */ - static var hamsterStandardKeyboardBackground: Color { - color(for: .standardKeyboardBackground) - } - - /** - The standard keyboard background color in dark keyboard - appearance. - */ - static var hamsterStandardKeyboardBackgroundForDarkAppearance: Color { - color(for: .standardKeyboardBackgroundForDarkAppearance) - } -} - -// MARK: - Functions - -public extension KeyboardColorReader { - /** - The standard background color of light keyboard buttons. - */ - static func hamsterStandardButtonBackground(for context: KeyboardContext) -> Color { - context.hasDarkColorScheme ? - .hamsterStandardButtonBackgroundForColorSchemeBug : - .hamsterStandardButtonBackground - } - - /** - The standard foreground color of light keyboard buttons. - */ - static func hamsterStandardButtonForeground(for context: KeyboardContext) -> Color { - context.hasDarkColorScheme ? - .hamsterStandardButtonForegroundForDarkAppearance : - .hamsterStandardButtonForeground - } - - /** - The standard shadow color of keyboard buttons. - */ - static func hamsterStandardButtonShadow(for context: KeyboardContext) -> Color { - .hamsterStandardButtonShadow - } - - /** - The standard background color of dark keyboard buttons. - */ - static func hamsterStandardDarkButtonBackground(for context: KeyboardContext) -> Color { - context.hasDarkColorScheme ? - .hamsterStandardDarkButtonBackgroundForColorSchemeBug : - .hamsterStandardDarkButtonBackground - } - - /** - The standard foreground color of dark keyboard buttons. - */ - static func hamsterStandardDarkButtonForeground(for context: KeyboardContext) -> Color { - context.hasDarkColorScheme ? - .hamsterStandardDarkButtonForegroundForDarkAppearance : - .hamsterStandardDarkButtonForeground - } -} - -private extension KeyboardColorReader { - static func color(for color: HamsterKeyboardColor) -> Color { - color.color - } -} - -struct KeyboardColorReader_Previews: PreviewProvider { - static func preview(for color: Color, name: String) -> some View { - VStack(alignment: .leading) { - Text(name).font(.footnote) - HStack(spacing: 0) { - color - color.colorScheme(.dark) - } - .frame(height: 100) - .cornerRadius(10) - } - } - - static var previews: some View { - ScrollView { - VStack { - Group { - preview(for: .standardButtonBackground, name: "standardButtonBackground") - preview(for: .standardButtonBackgroundForColorSchemeBug, name: "standardButtonBackgroundForColorSchemeBug") - preview(for: .standardButtonBackgroundForDarkAppearance, name: "standardButtonBackgroundForDarkAppearance") - preview(for: .standardButtonForeground, name: "standardButtonForeground") - preview(for: .standardButtonForegroundForDarkAppearance, name: "standardButtonForegroundForDarkAppearance") - preview(for: .standardButtonShadow, name: "standardButtonShadow") - } - Group { - preview(for: .standardDarkButtonBackground, name: "standardDarkButtonBackground") - preview(for: .standardDarkButtonBackgroundForColorSchemeBug, name: "standardDarkButtonBackgroundForColorSchemeBug") - preview(for: .standardDarkButtonBackgroundForDarkAppearance, name: "standardDarkButtonBackgroundForDarkAppearance") - preview(for: .standardDarkButtonForeground, name: "standardDarkButtonForeground") - preview(for: .standardDarkButtonForegroundForDarkAppearance, name: "standardDarkButtonForegroundForDarkAppearance") - preview(for: .standardKeyboardBackground, name: "standardKeyboardBackground") - preview(for: .standardKeyboardBackgroundForDarkAppearance, name: "standardKeyboardBackgroundForDarkAppearance") - } - }.padding() - }.background(Color.black.opacity(0.1).edgesIgnoringSafeArea(.all)) - } -} diff --git a/General/Lab/Keybaord/Feedback/KeyboardFeedbackHandler.swift b/General/Lab/Keybaord/Feedback/KeyboardFeedbackHandler.swift deleted file mode 100644 index 6d035e41..00000000 --- a/General/Lab/Keybaord/Feedback/KeyboardFeedbackHandler.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// KeyboardFeedbackHandler.swift -// HamsterKeyboard -// -// Created by morse on 16/3/2023. -// - -import KeyboardKit - -class HamsterKeyboardFeedbackHandler: StandardKeyboardFeedbackHandler { - var appSettings: HamsterAppSettings - - init(settings: KeyboardFeedbackSettings, appSettings: HamsterAppSettings) { - self.appSettings = appSettings - super.init(settings: settings) - } - - override func triggerFeedback(for gesture: KeyboardGesture, on action: KeyboardAction) { - if appSettings.enableKeyboardFeedbackSound { - triggerAudioFeedback(for: gesture, on: action) - } - - if appSettings.enableKeyboardFeedbackHaptic { - var hapticFeedfack: HapticFeedback = .mediumImpact - if let hapticIntensity = HapticIntensity(rawValue: appSettings.keyboardFeedbackHapticIntensity) { - switch hapticIntensity { - case .ultraLightImpact: - hapticFeedfack = .selectionChanged - case .lightImpact: - hapticFeedfack = .lightImpact - case .mediumImpact: - hapticFeedfack = .mediumImpact - case .heavyImpact: - hapticFeedfack = .heavyImpact - } - } - HapticFeedback.engine.trigger(hapticFeedfack) - } - } - - override func triggerAudioFeedback(for gesture: KeyboardGesture, on action: KeyboardAction) { - let custom = audioConfig.actions.first { $0.action == action } - if let custom = custom { return custom.feedback.trigger() } - if action == .space && gesture == .longPress { return } - if action == .backspace { return audioConfig.delete.trigger() } - if action.isHamsterInputAction { return audioConfig.input.trigger() } - if action.isSystemAction { return audioConfig.system.trigger() } - } -} diff --git a/General/Lab/Keybaord/Gestures/HamsterSwipeGestureHandler.swift b/General/Lab/Keybaord/Gestures/HamsterSwipeGestureHandler.swift deleted file mode 100644 index f9ab0497..00000000 --- a/General/Lab/Keybaord/Gestures/HamsterSwipeGestureHandler.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// HamsterSlidingGestureHandler.swift -// HamsterKeyboard -// -// Created by morse on 23/4/2023. -// - -import Foundation -import KeyboardKit - -/// 滑动方向 -enum SwipeDirection: Equatable { - case up - case down - case left - case right - - // 是否X轴滑动 - var isXAxis: Bool { - self == .left || self == .right - } - - var isYAxis: Bool { - self == .up || self == .down - } -} - -/// 全键盘手势滑动处理 -class HamsterSwipeGestureHandler: SwipeGestureHandler { - typealias OffsetX = Int - typealias OffsetY = Int - typealias SwipeGestureAction = (KeyboardAction, SwipeDirection, OffsetX, OffsetY) -> Void - public init( - keyboardContext: KeyboardContext, - // 滑动敏感度:滑动超过阈值才有效 - sensitivityX: SpaceDragSensitivity = .custom(points: 10), - sensitivityY: SpaceDragSensitivity = .custom(points: 20), - action: @escaping SwipeGestureAction - ) { - self.keyboardContext = keyboardContext - self.sensitivityY = sensitivityY - self.sensitivityX = sensitivityX - self.action = action - } - - var isDragging: Bool = false - - private let enableSpaceSliding: Bool = false - public let keyboardContext: KeyboardContext - public let sensitivityX: SpaceDragSensitivity - public let sensitivityY: SpaceDragSensitivity - public let action: SwipeGestureAction - - // 当前触发开始的Action - public var currentAction: KeyboardAction? - // 当前触发开始位置Location - // 系统每次调用Drag时, 如果startLocation不同, 则视为一个新的startLocation - public var currentDragStartLocation: CGPoint? - - // Action完成标志 - private var currentActionIsFinished = false - - // 保留从开始位置到结束位置y轴偏移量(偏移量需要除以sensitivity的值, 已确定是否超过阈值) - public var currentDragOffsetY: Int = 0 - - // 保留从开始位置到结束位置X轴偏移量(偏移量需要除以sensitivity的值, 已确定是否超过阈值) - public var currentDragOffsetX: Int = 0 - - public func handleDragGesture(action: KeyboardAction, from startLocation: CGPoint, to currentLocation: CGPoint) { - let isNewAction = action == currentAction - if isNewAction { - currentAction = action - currentDragOffsetY = 0 - currentDragOffsetX = 0 - currentActionIsFinished = false - } - - tryStartNewDragGesture(from: startLocation, to: currentLocation) - let dragDeltaY = startLocation.y - currentLocation.y - let dragDeltaX = startLocation.x - currentLocation.x - let dragOffsetY = Int(dragDeltaY / CGFloat(sensitivityY.points)) - let dragOffsetX = Int(dragDeltaX / CGFloat(sensitivityX.points)) - // x,y轴都没有超过阈值,则不视做一次滑动 - if dragOffsetX == currentDragOffsetX && dragOffsetY == currentDragOffsetY { - return - } - - // 滑动方向 - var slidingDirection: SwipeDirection - - if dragOffsetX == 0 && dragOffsetY != 0 { // 上下滑动 - slidingDirection = dragOffsetY > 0 ? .up : .down - } else if dragOffsetY == 0 && dragOffsetX != 0 { // 左右滑动 - slidingDirection = dragOffsetX > 0 ? .left : .right - } else { // 其余情况已左右滑动优先 - slidingDirection = dragOffsetX > 0 ? .left : .right - } - - // TODO:目前只有空格左右滑动可以连续触发, 其余Action都是一次性触发 - if action == .space && slidingDirection.isXAxis { - isDragging = true - let offsetDelta = dragOffsetX - currentDragOffsetX - self.action(action, slidingDirection, -offsetDelta, 0) - } else if !currentActionIsFinished { - isDragging = true - self.action(action, slidingDirection, dragOffsetX, dragOffsetY) - currentActionIsFinished = true - } - - currentDragOffsetX = dragOffsetX - currentDragOffsetY = dragOffsetY - } - - func endDragGesture() { - currentAction = nil - currentDragOffsetX = 0 - currentDragOffsetY = 0 - currentActionIsFinished = false - isDragging = false - } - - func tryStartNewDragGesture( - from startLocation: CGPoint, - to currentLocation: CGPoint - ) { - let isNewDrag = currentDragStartLocation != startLocation - currentDragStartLocation = startLocation - guard isNewDrag else { return } - currentDragOffsetY = 0 - currentDragOffsetX = 0 - } -} diff --git a/General/Lab/Keybaord/Gestures/SwipeGestureHandler.swift b/General/Lab/Keybaord/Gestures/SwipeGestureHandler.swift deleted file mode 100644 index 8ff0940e..00000000 --- a/General/Lab/Keybaord/Gestures/SwipeGestureHandler.swift +++ /dev/null @@ -1,14 +0,0 @@ -import CoreGraphics -import KeyboardKit - -// 滑动手势 -public protocol SwipeGestureHandler { - // 拖拽进行中 - var isDragging: Bool { get set } - - func handleDragGesture( - action: KeyboardAction, from startLocation: CGPoint, to currentLocation: CGPoint - ) - - func endDragGesture() -} diff --git a/General/Lab/Keybaord/Image/KeyboardImageReader.swift b/General/Lab/Keybaord/Image/KeyboardImageReader.swift deleted file mode 100644 index 388a29ea..00000000 --- a/General/Lab/Keybaord/Image/KeyboardImageReader.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// KeyboardImageReader.swift -// HamsterKeyboard -// -// Created by morse on 4/3/2023. -// - -import KeyboardKit -import SwiftUI - -extension KeyboardImageReader { - -} diff --git a/General/Lab/Keybaord/Keyboard/KeyboardType+Button.swift b/General/Lab/Keybaord/Keyboard/KeyboardType+Button.swift deleted file mode 100644 index f4d507e0..00000000 --- a/General/Lab/Keybaord/Keyboard/KeyboardType+Button.swift +++ /dev/null @@ -1,21 +0,0 @@ -import KeyboardKit -import SwiftUI - -extension KeyboardType { - /** - The keyboard type's standard button text. - */ - public func hamsterButtonText(for context: KeyboardContext) -> String? { - switch self { - case .alphabetic: return KKL10n.keyboardTypeAlphabetic.hamsterText(for: context) - case .numeric: return KKL10n.keyboardTypeNumeric.hamsterText(for: context) - case .symbolic: return KKL10n.keyboardTypeSymbolic.hamsterText(for: context) - case .custom(let name): - if let customKeyboard = keyboardCustomType(rawValue: name) { - return customKeyboard.buttonName - } - return nil - default: return nil - } - } -} diff --git a/General/Lab/Keybaord/Keyboard/KeyboardType.swift b/General/Lab/Keybaord/Keyboard/KeyboardType.swift deleted file mode 100644 index 8a5d0d39..00000000 --- a/General/Lab/Keybaord/Keyboard/KeyboardType.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// KeyboardType.swift -// Hamster -// -// Created by morse on 19/5/2023. -// - -import Foundation -import KeyboardKit - -enum keyboardCustomType: String, CaseIterable, Equatable { - // "拼音九宫格" - case chineseNineGrid - - // "数字九宫格" - case numberNineGrid - - // 符号 - case symbol - - var buttonName: String { - switch self { - case .numberNineGrid: return "123" - case .symbol: return "符" - default: return "" - } - } - - var keyboardType: KeyboardType { - return .custom(named: self.rawValue) - } - - var keyboardAction: KeyboardAction? { - switch self { - case .numberNineGrid: return .keyboardType(.custom(named: self.rawValue)) - case .symbol: return .keyboardType(.custom(named: self.rawValue)) - default: return nil - } - } -} - -extension KeyboardType { - /** - * 是否数字九宫格 - */ - var isNumberNineGrid: Bool { - switch self { - case .custom(let name): - if name == keyboardCustomType.numberNineGrid.rawValue { - return true - } - return false - default: return false - } - } - - /** - * 是否数字九宫格 - */ - var isCustomSymbol: Bool { - switch self { - case .custom(let name): - if name == keyboardCustomType.symbol.rawValue { - return true - } - return false - default: return false - } - } -} diff --git a/General/Lab/Keybaord/KeyboardConstants.swift b/General/Lab/Keybaord/KeyboardConstants.swift deleted file mode 100644 index 074325e3..00000000 --- a/General/Lab/Keybaord/KeyboardConstants.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation - -class KeyboardConstant { - enum Character: String { - case equal = "=" - case plus = "+" - case minus = "-" - case asterisk = "*" - case slash = "/" - case backspace // - - static let SlideUp = "↑" // 表示上滑 Upwards Arrow: https://www.compart.com/en/unicode/U+2191 - static let SlideDown = "↓" // 表示下滑 Downwards Arrow: https://www.compart.com/en/unicode/U+2193 - static let SlideLeft = "←" // 表示左滑 Leftwards Arrow: https://www.compart.com/en/unicode/U+2190 - static let SlideRight = "→" // 表示右滑 Rightwards Arrow: https://www.compart.com/en/unicode/U+2192 - } - - enum Action { - static let endDragGesture = "endDragGesture" - } - - enum ImageName { - static let switchLanguage = "switchLanguage" - static let chineseLanguageImageName = "cn" - static let englishLanguageImageName = "en" - } -} diff --git a/General/Lab/Keybaord/KeyboardView/HamsterGestureButton.swift b/General/Lab/Keybaord/KeyboardView/HamsterGestureButton.swift deleted file mode 100644 index f8390c31..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterGestureButton.swift +++ /dev/null @@ -1,370 +0,0 @@ -// -// HamsterGestureButton.swift -// Hamster -// -// Created by morse on 8/5/2023. -// - -import KeyboardKit -import SwiftUI - -/** - This button uses a single drag gesture to implement support - for a bunch of different gestures. - - This button can not be used within a `ScrollView`, since it - will block the scroll gestures. For these cases, you should - consider using a ``ScrollViewGestureButton`` instead. - */ -public struct HamsterGestureButton: View { - /** - Create a drag gesture button. - - - Parameters: - - isPressed: A custom, optional binding to track pressed state, by default `nil`. - - pressAction: The action to trigger when the button is pressed, by default `nil`. - - releaseInsideAction: The action to trigger when the button is released inside, by default `nil`. - - releaseOutsideAction: The action to trigger when the button is released outside of its bounds, by default `nil`. - - longPressDelay: The time it takes for a press to count as a long press, by default ``GestureButtonDefaults/longPressDelay``. - - longPressAction: The action to trigger when the button is long pressed, by default `nil`. - - doubleTapTimeout: The max time between two taps for them to count as a double tap, by default ``GestureButtonDefaults/doubleTapTimeout``. - - doubleTapAction: The action to trigger when the button is double tapped, by default `nil`. - - repeatDelay: The time it takes for a press to count as a repeat trigger, by default ``GestureButtonDefaults/repeatDelay``. - - repeatTimer: The repeat timer to use for the repeat action, by default ``RepeatGestureTimer/shared``. - - repeatAction: The action to repeat while the button is being pressed, by default `nil`. - - dragStartAction: The action to trigger when a drag gesture starts. - - dragAction: The action to trigger when a drag gesture changes. - - dragEndAction: The action to trigger when a drag gesture ends. - - endAction: The action to trigger when a button gesture ends, by default `nil`. - - label: The button label. - */ - init( - isPressed: Binding? = nil, - pressAction: Action? = nil, - releaseInsideAction: Action? = nil, - releaseOutsideAction: Action? = nil, - longPressDelay: TimeInterval = GestureButtonDefaults.longPressDelay, - longPressAction: Action? = nil, - doubleTapTimeout: TimeInterval = GestureButtonDefaults.doubleTapTimeout, - doubleTapAction: Action? = nil, - repeatDelay: TimeInterval = GestureButtonDefaults.repeatDelay, - repeatTimer: RepeatGestureTimer = .shared, - repeatAction: Action? = nil, - dragStartAction: DragAction? = nil, - dragAction: DragAction? = nil, - dragEndAction: DragAction? = nil, - endAction: Action? = nil, - label: @escaping LabelBuilder - ) { - self.isPressedBinding = isPressed ?? .constant(false) - self.pressAction = pressAction - self.releaseInsideAction = releaseInsideAction - self.releaseOutsideAction = releaseOutsideAction - self.longPressDelay = longPressDelay - self.longPressAction = longPressAction - self.doubleTapTimeout = doubleTapTimeout - self.doubleTapAction = doubleTapAction - self.repeatDelay = repeatDelay - self.repeatTimer = repeatTimer - self.repeatAction = repeatAction - self.dragStartAction = dragStartAction - self.dragAction = dragAction - self.dragEndAction = dragEndAction - self.endAction = endAction - self.label = label - } - - public typealias Action = () -> Void - public typealias DragAction = (DragGesture.Value) -> Void - public typealias LabelBuilder = (_ isPressed: Bool) -> Label - - var isPressedBinding: Binding - - let pressAction: Action? - let releaseInsideAction: Action? - let releaseOutsideAction: Action? - let longPressDelay: TimeInterval - let longPressAction: Action? - let doubleTapTimeout: TimeInterval - let doubleTapAction: Action? - let repeatDelay: TimeInterval - let repeatTimer: RepeatGestureTimer - let repeatAction: Action? - let dragStartAction: DragAction? - let dragAction: DragAction? - let dragEndAction: DragAction? - let endAction: Action? - let label: LabelBuilder - - @State - private var isPressed = false - - @State - private var longPressDate = Date() - - @State - private var releaseDate = Date() - - @State - private var repeatDate = Date() - - @EnvironmentObject - private var actionHandler: HamsterKeyboardActionHandler - - public var body: some View { - label(isPressed) - .overlay(gestureView) - .onChange(of: isPressed) { isPressedBinding.wrappedValue = $0 } - .accessibilityAddTraits(.isButton) - } -} - -private extension HamsterGestureButton { - var gestureView: some View { - GeometryReader { geo in - Color.clear - .contentShape(Rectangle()) - .gesture( - DragGesture(minimumDistance: 0) - .onChanged { value in - tryHandlePress(value, in: geo) - if !actionHandler.isScrolling { - dragAction?(value) - } - // fix 候选栏水平滚动期间误触的按键,停止滚动后显示为按压状态 - // 超过一定时间后重置 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - if actionHandler.isScrolling, isPressed { - isPressed = false - } - } - } - .onEnded { value in - tryHandleRelease(value, in: geo) - } - ) - } - } -} - -private extension HamsterGestureButton { - func tryHandlePress(_ value: DragGesture.Value, in geo: GeometryProxy) { - if isPressed { return } - isPressed = true - pressAction?() - if !actionHandler.isScrolling { - dragStartAction?(value) - tryTriggerLongPressAfterDelay() - tryTriggerRepeatAfterDelay() - } - } - - func tryHandleRelease(_ value: DragGesture.Value, in geo: GeometryProxy) { - if !isPressed { return } - isPressed = false - longPressDate = Date() - repeatDate = Date() - repeatTimer.stop() - releaseDate = tryTriggerDoubleTap() ? .distantPast : Date() - dragEndAction?(value) - if geo.contains(value.location) { - releaseInsideAction?() - } else { - releaseOutsideAction?() - } - endAction?() - } - - func tryTriggerLongPressAfterDelay() { - guard let action = longPressAction else { return } - let date = Date() - longPressDate = date - let delay = longPressDelay - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - guard self.longPressDate == date else { return } - action() - } - } - - func tryTriggerRepeatAfterDelay() { - guard let action = repeatAction else { return } - let date = Date() - repeatDate = date - let delay = repeatDelay - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - guard self.repeatDate == date else { return } - repeatTimer.start(action: action) - } - } - - func tryTriggerDoubleTap() -> Bool { - let interval = Date().timeIntervalSince(releaseDate) - let isDoubleTap = interval < doubleTapTimeout - if isDoubleTap { doubleTapAction?() } - return isDoubleTap - } -} - -private extension GeometryProxy { - func contains(_ dragEndLocation: CGPoint) -> Bool { - let x = dragEndLocation.x - let y = dragEndLocation.y - guard x > 0, y > 0 else { return false } - guard x < size.width, y < size.height else { return false } - return true - } -} - -// struct GestureButton_Previews: PreviewProvider { -// struct Preview: View { -// @StateObject -// var state = PreviewState() -// -// @State -// private var items = (1...3).map { PreviewItem(id: $0) } -// -// var body: some View { -// VStack(spacing: 20) { -// PreviewHeader(state: state) -// .padding(.horizontal) -// -// PreviewButtonGroup(title: "Buttons:") { -// HamsterGestureButton( -// isPressed: $state.isPressed, -// pressAction: { state.pressCount += 1 }, -// releaseInsideAction: { state.releaseInsideCount += 1 }, -// releaseOutsideAction: { state.releaseOutsideCount += 1 }, -// longPressDelay: 0.8, -// longPressAction: { state.longPressCount += 1 }, -// doubleTapAction: { state.doubleTapCount += 1 }, -// repeatAction: { state.repeatTapCount += 1 }, -// dragStartAction: { state.dragStartedValue = $0.location }, -// dragAction: { state.dragChangedValue = $0.location }, -// dragEndAction: { state.dragEndedValue = $0.location }, -// endAction: { state.endCount += 1 }, -// label: { PreviewButton(color: .blue, isPressed: $0) } -// ) -// } -// } -// } -// } -// -// struct PreviewItem: Identifiable { -// var id: Int -// } -// -// struct PreviewButton: View { -// let color: Color -// let isPressed: Bool -// -// var body: some View { -// color -// .cornerRadius(10) -// .opacity(isPressed ? 0.5 : 1) -// .scaleEffect(isPressed ? 0.9 : 1) -// .animation(.default, value: isPressed) -// .padding() -// .background(Color.random()) -// .cornerRadius(16) -// } -// } -// -// struct PreviewButtonGroup: View { -// let title: String -// let button: () -> Content -// -// var body: some View { -// VStack(alignment: .leading, spacing: 5) { -// Text(title) -// HStack { -// ForEach(0...3, id: \.self) { _ in -// button() -// } -// }.frame(maxWidth: .infinity) -// }.padding(.horizontal) -// } -// } -// -// class PreviewState: ObservableObject { -// @Published -// var isPressed = false -// -// @Published -// var pressCount = 0 -// -// @Published -// var releaseInsideCount = 0 -// -// @Published -// var releaseOutsideCount = 0 -// -// @Published -// var endCount = 0 -// -// @Published -// var longPressCount = 0 -// -// @Published -// var doubleTapCount = 0 -// -// @Published -// var repeatTapCount = 0 -// -// @Published -// var dragStartedValue = CGPoint.zero -// -// @Published -// var dragChangedValue = CGPoint.zero -// -// @Published -// var dragEndedValue = CGPoint.zero -// } -// -// struct PreviewHeader: View { -// @ObservedObject -// var state: PreviewState -// -// var body: some View { -// VStack(alignment: .leading) { -// Group { -// label("Pressed", state.isPressed ? "YES" : "NO") -// label("Presses", state.pressCount) -// label("Releases", state.releaseInsideCount + state.releaseOutsideCount) -// label(" Inside", state.releaseInsideCount) -// label(" Outside", state.releaseOutsideCount) -// label("Ended", state.endCount) -// label("Long presses", state.longPressCount) -// label("Double taps", state.doubleTapCount) -// label("Repeats", state.repeatTapCount) -// } -// Group { -// label("Drag started", state.dragStartedValue) -// label("Drag changed", state.dragChangedValue) -// label("Drag ended", state.dragEndedValue) -// } -// } -// .frame(maxWidth: .infinity, alignment: .leading) -// .padding() -// .background(RoundedRectangle(cornerRadius: 16).stroke(.blue, lineWidth: 3)) -// } -// -// func label(_ title: String, _ int: Int) -> some View { -// label(title, "\(int)") -// } -// -// func label(_ title: String, _ point: CGPoint) -> some View { -// label(title, "\(point.x.rounded()), \(point.y.rounded())") -// } -// -// func label(_ title: String, _ value: String) -> some View { -// HStack { -// Text("\(title):") -// Text(value).bold() -// }.lineLimit(1) -// } -// } -// -// static var previews: some View { -// Preview() -// } -// } diff --git a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonBody.swift b/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonBody.swift deleted file mode 100644 index 29b8f3f7..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonBody.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// HamsterKeyboardButtonBody.swift -// Hamster -// -// Created by morse on 2023/5/26. -// - -import KeyboardKit -import SwiftUI - -struct HamsterKeyboardButtonBody: View { - /** - Create a system keyboard button body view. - - - Parameters: - - style: The button style to apply. - - isPressed: Whether or not the button is pressed, by default `false`. - */ - public init( - style: KeyboardButtonStyle, - isPressed: Bool = false - ) { - self.style = style - self.isPressed = isPressed - } - - private let style: KeyboardButtonStyle - private let isPressed: Bool - - @EnvironmentObject - var keyboardContext: KeyboardContext - - @EnvironmentObject - var appSettings: HamsterAppSettings - - public var body: some View { - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(borderColor, lineWidth: borderLineWidth) - .background(backgroundColor) - .overlay(isPressed ? style.pressedOverlayColor : .clear) - .cornerRadius(cornerRadius) - .overlay(SystemKeyboardButtonShadow(style: style)) - } -} - -extension HamsterKeyboardButtonBody { - var backgroundColor: Color { - style.backgroundColor ?? .clear - } - - var borderColor: Color { - style.border?.color ?? .clear - } - - var borderLineWidth: CGFloat { - style.border?.size ?? 0 - } - - var cornerRadius: CGFloat { - style.cornerRadius ?? 0 - } -} - -extension KeyboardButtonBorderStyle { - /** - This internal style is only used in previews. - */ - static let previewStyle1 = KeyboardButtonBorderStyle( - color: .red, - size: 3 - ) - - /** - This internal style is only used in previews. - */ - static let previewStyle2 = KeyboardButtonBorderStyle( - color: .blue, - size: 5 - ) -} - -extension KeyboardButtonShadowStyle { - /** - This internal style is only used in previews. - */ - static let previewStyle1 = KeyboardButtonShadowStyle( - color: .blue, - size: 4 - ) - - /** - This internal style is only used in previews. - */ - static let previewStyle2 = KeyboardButtonShadowStyle( - color: .green, - size: 8 - ) -} - -extension KeyboardButtonStyle { - /** - This internal style is only used in previews. - */ - static let preview1 = KeyboardButtonStyle( - backgroundColor: .yellow, - foregroundColor: .white, - font: .body, - cornerRadius: 20, - border: KeyboardButtonBorderStyle.previewStyle1, - shadow: KeyboardButtonShadowStyle.previewStyle1 - ) - - /** - This internal style is only used in previews. - */ - static let preview2 = KeyboardButtonStyle( - backgroundColor: .purple, - foregroundColor: .yellow, - font: .headline, - cornerRadius: 10, - border: KeyboardButtonBorderStyle.previewStyle2, - shadow: KeyboardButtonShadowStyle.previewStyle2 - ) -} - -struct HamsterKeyboardButtonBody_Previews: PreviewProvider { - static var previews: some View { - VStack { - HamsterKeyboardButtonBody(style: KeyboardButtonStyle.preview1) - HamsterKeyboardButtonBody(style: KeyboardButtonStyle.preview2) - } - .padding() - .background(Color.gray) - .cornerRadius(10) - .environment(\.sizeCategory, .extraExtraLarge) - } -} diff --git a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonContent.swift b/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonContent.swift deleted file mode 100644 index 5713bbad..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonContent.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// HamsterKeyboardActionButtonContent.swift -// HamsterKeyboard -// -// Created by morse on 11/1/2023. -// - -import KeyboardKit -import SwiftUI - -@available(iOS 14, *) -struct HamsterKeyboardButtonContent: View { - /** - Create a system keyboard button content view. - - - Parameters: - - action: The action for which to generate content. - - appearance: The appearance to apply to the content. - - context: The context to use when resolving content. - */ - public init( - action: KeyboardAction, - appearance: KeyboardAppearance, - keyboardContext: KeyboardContext - ) { - self.action = action - self.appearance = appearance as! HamsterKeyboardAppearance - self.keyboardContext = keyboardContext - } - - private let keyboardContext: KeyboardContext - private let action: KeyboardAction - private let appearance: HamsterKeyboardAppearance - - @EnvironmentObject var appSettings: HamsterAppSettings - @EnvironmentObject var rimeContext: RimeContext - - public var body: some View { - bodyContent - .padding(3) - .contentShape(Rectangle()) - } -} - -private extension HamsterKeyboardButtonContent { - @ViewBuilder - var bodyContent: some View { -#if os(iOS) || os(tvOS) - if action == .nextKeyboard { - NextKeyboardButton { bodyView } - } else { - bodyView - } -#else - bodyView -#endif - } - - @ViewBuilder - var bodyView: some View { - if action == .space { - spaceView - } else if let image = appearance.buttonImage(for: action) { - image.scaleEffect(appearance.buttonImageScaleFactor(for: action)) - } else if let text = appearance.buttonText(for: action) { - textView(for: text) - } else { - Text("") - } - } - - @ViewBuilder - var spaceView: some View { - // HamsterSystemKeyboardSpaceContent( - // localeText: localSpaceText, - // spaceView: localSpaceView - // ) - SystemKeyboardButtonText( - text: keyboardContext.keyboardType.isNumberNineGrid ? "空格" : localSpaceText, - action: .space - ) - .minimumScaleFactor(0.5) - } - - func textView(for text: String) -> some View { - // SystemKeyboardButtonText( - // text: text, - // action: action - // ).minimumScaleFactor(0.5) - HamsterKeyboardButtonText( - buttonExtendCharacter: appearance.buttonExtendCharacter, - text: text, - isInputAction: action.isInputAction || { - switch action { - case .custom: - return true - default: - return false - } - }() - ) - .minimumScaleFactor(0.5) - } -} - -private extension HamsterKeyboardButtonContent { - var spaceText: String { - KKL10n.hamsterText(forKey: "space", locale: keyboardContext.locale) - } - - var localSpaceText: String { - if rimeContext.asciiMode { - return "EN" - } - return appSettings.rimeTotalSchemas.first { - $0.schemaId == appSettings.rimeInputSchema - }?.schemaName ?? "" - } - - var localSpaceView: some View { - if #available(iOS 16, *) { - return Image(systemName: "space") - } - return Text(KKL10n.hamsterText(forKey: "space", locale: keyboardContext.locale)) - } -} - -// private extension HamsterKeyboardActionButtonContent { -// var spaceText: String { -// appearance.buttonText(for: action) ?? "" -// } -// } diff --git a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonText.swift b/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonText.swift deleted file mode 100644 index 5a8fa59d..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardButtonText.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// KeyboardButtonText.swift -// HamsterKeyboard -// -// Created by morse on 11/1/2023. -// - -import KeyboardKit -import SwiftUI - -@available(iOS 14, *) -struct HamsterKeyboardButtonText: View { - public init( - buttonExtendCharacter: [String: [String]], - text: String, - action: KeyboardAction - ) { - self.init( - buttonExtendCharacter: buttonExtendCharacter, - text: text, - isInputAction: action.isInputAction - ) - } - - public init( - buttonExtendCharacter: [String: [String]], - text: String, - isInputAction: Bool - ) { - self.buttonExtendCharacter = buttonExtendCharacter - self.text = text - self.isInputAction = isInputAction - } - - var buttonExtendCharacter: [String: [String]] - private let text: String - private let isInputAction: Bool - - @EnvironmentObject var appSettings: HamsterAppSettings - @EnvironmentObject var keyboardContext: KeyboardContext - - var characterExtendView: some View { - let texts = buttonExtendCharacter[text.lowercased(), default: []] - return HStack(alignment: .center, spacing: 0) { - if !texts.isEmpty { - Text(texts[0]) - } - if texts.count > 1 { - Spacer() - Text(texts[1]) - } - } - .opacity(0.64) - .font(.system(size: 7)) - .frame(minWidth: 0, maxWidth: .infinity) - .frame(height: 10) - .padding(.init(top: 2, leading: 1, bottom: 1, trailing: 1)) - .lineLimit(1) - .minimumScaleFactor(0.5) - } - - public var body: some View { - let showExtendArea = showExtendArea - ZStack(alignment: .center) { - if showExtendArea { - VStack(alignment: .center, spacing: 0) { - characterExtendView - - Spacer() - } - .offset(y: -4) - } - Text(text) - .lineLimit(1) - .offset(y: useNegativeOffset ? -2 : 0) - .scaleEffect(showExtendArea && isInputAction ? 0.85 : 1) - } - } -} - -private extension HamsterKeyboardButtonText { - // 使用负偏移 - var useNegativeOffset: Bool { - isInputAction && text.isLowercased - } - - // 是否显示按键扩展区域 - var showExtendArea: Bool { - appSettings.enableKeyboardSwipeGestureSymbol - && appSettings.showKeyExtensionArea - && keyboardContext.keyboardType.isAlphabetic - } -} - -struct HamsterKeyboardButtonText_Previews: PreviewProvider { - static var previews: some View { - HStack { - SystemKeyboardButtonText(text: "PasCal", action: .space) - SystemKeyboardButtonText(text: "UPPER", action: .space) - SystemKeyboardButtonText(text: "lower", action: .space) - SystemKeyboardButtonText(text: "lower", action: .space) - SystemKeyboardButtonText(text: "non-input", action: .backspace) - } - } -} diff --git a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardGestures.swift b/General/Lab/Keybaord/KeyboardView/HamsterKeyboardGestures.swift deleted file mode 100644 index 9e6e0322..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterKeyboardGestures.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// HamsterKeyboardGestures.swift -// Hamster -// -// Created by morse on 8/5/2023. -// - -import KeyboardKit -import SwiftUI - -/** - This view wraps any view and applies a ``GestureButton`` to - it, to handle all actions for the provided keyboard action. - - This view is internal. Apply it with `View+keyboardGestures`. - */ -struct HamsterKeyboardGestures: View { - /** - Apply a set of optional gesture actions to the provided - view, for a certain keyboard action. - - - Parameters: - - view: The view to apply the gestures to. - - action: The keyboard action to trigger. - - calloutContext: The callout context to affect, if any. - - isInScrollView: Whether or not the gestures are used in a scroll view. - - isPressed: An optional binding that can be used to observe the button pressed state. - - doubleTapAction: The action to trigger when the button is double tapped. - - longPressAction: The action to trigger when the button is long pressed. - - pressAction: The action to trigger when the button is pressed. - - releaseAction: The action to trigger when the button is released, regardless of where the gesture ends. - - repeatAction: The action to trigger when the button is pressed and held. - - dragAction: The action to trigger when the button is dragged. - */ - init( - view: Content, - action: KeyboardAction?, - calloutContext: KeyboardCalloutContext?, - isInScrollView: Bool = false, - isPressed: Binding, - doubleTapAction: KeyboardGestureAction?, - longPressAction: KeyboardGestureAction?, - pressAction: KeyboardGestureAction?, - releaseAction: KeyboardGestureAction?, - repeatAction: KeyboardGestureAction?, - dragAction: KeyboardDragGestureAction? - ) { - self.view = view - self.action = action - self.calloutContext = calloutContext - self.isInScrollView = isInScrollView - self.isPressed = isPressed - self.doubleTapAction = doubleTapAction - self.longPressAction = longPressAction - self.pressAction = pressAction - self.releaseAction = releaseAction - self.repeatAction = repeatAction - self.dragAction = dragAction - } - - private let view: Content - private let action: KeyboardAction? - private let calloutContext: KeyboardCalloutContext? - private let isInScrollView: Bool - private let isPressed: Binding - private let doubleTapAction: KeyboardGestureAction? - private let longPressAction: KeyboardGestureAction? - private let pressAction: KeyboardGestureAction? - private let releaseAction: KeyboardGestureAction? - private let repeatAction: KeyboardGestureAction? - private let dragAction: KeyboardDragGestureAction? - - @State - private var isPressGestureActive = false { - didSet { isPressed.wrappedValue = isPressGestureActive } - } - - @State - private var shouldApplyReleaseAction = true - - @EnvironmentObject - private var actionHandler: HamsterKeyboardActionHandler - - var body: some View { - view.overlay( - GeometryReader { geo in - gestureButton(for: geo) - } - ) - } -} - -private extension View { - @ViewBuilder - func optionalGesture(_ gesture: GestureType?) -> some View { - if let gesture = gesture { - self.gesture(gesture) - } else { - self - } - } -} - -// MARK: - Views - -private extension HamsterKeyboardGestures { - @ViewBuilder - func gestureButton(for geo: GeometryProxy) -> some View { - if isInScrollView { - HamsterScrollViewGestureButton( - isPressed: isPressed, - pressAction: { handlePress(in: geo) }, - releaseInsideAction: { handleReleaseInside(in: geo) }, - releaseOutsideAction: { handleReleaseOutside(in: geo) }, - longPressAction: { handleLongPress(in: geo) }, - doubleTapAction: { handleDoubleTap(in: geo) }, - repeatAction: { handleRepeat(in: geo) }, - dragAction: { handleDrag(in: geo, value: $0) }, - endAction: { handleGestureEnded(in: geo) }, - label: { _ in Color.clearInteractable } - ) - } else { - HamsterGestureButton( - isPressed: isPressed, - pressAction: { handlePress(in: geo) }, - releaseInsideAction: { handleReleaseInside(in: geo) }, - releaseOutsideAction: { handleReleaseOutside(in: geo) }, - longPressAction: { handleLongPress(in: geo) }, - doubleTapAction: { handleDoubleTap(in: geo) }, - repeatAction: { handleRepeat(in: geo) }, - dragAction: { handleDrag(in: geo, value: $0) }, - endAction: { handleGestureEnded(in: geo) }, - label: { _ in Color.clearInteractable } - ) - } - } -} - -// MARK: - Actions - -private extension HamsterKeyboardGestures { - func handleDoubleTap(in geo: GeometryProxy) { - doubleTapAction?() - } - - func handleDrag(in geo: GeometryProxy, value: DragGesture.Value) { - calloutContext?.action.updateSelection(with: value.translation) - dragAction?(value.startLocation, value.location) - } - - func handleGestureEnded(in geo: GeometryProxy) { - endActionCallout() - calloutContext?.input.resetWithDelay() - calloutContext?.action.reset() - shouldApplyReleaseAction = true - } - - func handleLongPress(in geo: GeometryProxy) { - shouldApplyReleaseAction = shouldApplyReleaseAction && action != .space - tryBeginActionCallout(in: geo) - longPressAction?() - } - - func handlePress(in geo: GeometryProxy) { - pressAction?() - if !actionHandler.isScrolling { - calloutContext?.input.updateInput(for: action, in: geo) - } - } - - func handleReleaseInside(in geo: GeometryProxy) { - updateShouldApplyReleaseAction() - guard shouldApplyReleaseAction else { return } - releaseAction?() - } - - func handleReleaseOutside(in geo: GeometryProxy) {} - - func handleRepeat(in geo: GeometryProxy) { - repeatAction?() - } - - func tryBeginActionCallout(in geo: GeometryProxy) { - guard let context = calloutContext?.action else { return } - context.updateInputs(for: action, in: geo) - guard context.isActive else { return } - calloutContext?.input.reset() - } - - func endActionCallout() { - calloutContext?.action.endDragGesture() - } - - func updateShouldApplyReleaseAction() { - guard let context = calloutContext?.action else { return } - shouldApplyReleaseAction = shouldApplyReleaseAction && !context.hasSelectedAction - } -} diff --git a/General/Lab/Keybaord/KeyboardView/HamsterScrollViewGestureButton.swift b/General/Lab/Keybaord/KeyboardView/HamsterScrollViewGestureButton.swift deleted file mode 100644 index efaafa82..00000000 --- a/General/Lab/Keybaord/KeyboardView/HamsterScrollViewGestureButton.swift +++ /dev/null @@ -1,448 +0,0 @@ -// -// HamsterScrollViewGestureButton.swift -// Hamster -// -// Created by morse on 8/5/2023. -// - -import KeyboardKit -import SwiftUI - -/** - This button can be used to apply a bunch of gestures to the - provided label, in a way that works within a `ScrollView`. - - This button can be used within a `ScrollView` since it will - not block the scroll view's scrolling, despite all gestures - that is applied to it. The code is complicated, since it is - the result of trial and many errors, where every change has - been tested to not affect the scrolling or any gestures. - - If you don't need to use a scroll view, you should consider - using a ``GestureButton`` instead. It's way more responsive, - since it uses a single drag gesture to trigger actions with - no delay, which however doesn't workin within a scroll view. - - Note that the view uses an underlying `ButtonStyle` to make - gestures work. It can thus not apply another style, but you - can use the `isPressed` value that is passed to the `label` - builder, to configure the button view for the pressed state. - */ -public struct HamsterScrollViewGestureButton: View { - /** - Create a gesture button. - - - Parameters: - - isPressed: A custom, optional binding to track pressed state, by default `nil`. - - pressAction: The action to trigger when the button is pressed, by default `nil`. - - releaseInsideAction: The action to trigger when the button is released inside, by default `nil`. - - releaseOutsideAction: The action to trigger when the button is released outside of its bounds, by default `nil`. - - longPressDelay: The time it takes for a press to count as a long press, by default ``GestureButtonDefaults/longPressDelay``. - - longPressAction: The action to trigger when the button is long pressed, by default `nil`. - - doubleTapTimeout: The max time between two taps for them to count as a double tap, by default ``GestureButtonDefaults/doubleTapTimeout``. - - doubleTapAction: The action to trigger when the button is double tapped, by default `nil`. - - repeatTimer: The repeat timer to use for the repeat action, by default ``RepeatGestureTimer/shared``. - - repeatAction: The action to repeat while the button is being pressed, by default `nil`. - - dragStartAction: The action to trigger when a drag gesture starts. - - dragAction: The action to trigger when a drag gesture changes. - - dragEndAction: The action to trigger when a drag gesture ends. - - endAction: The action to trigger when a button gesture ends, by default `nil`. - - label: The button label. - */ - init( - isPressed: Binding? = nil, - pressAction: Action? = nil, - releaseInsideAction: Action? = nil, - releaseOutsideAction: Action? = nil, - longPressDelay: TimeInterval = GestureButtonDefaults.longPressDelay, - longPressAction: Action? = nil, - doubleTapTimeout: TimeInterval = GestureButtonDefaults.doubleTapTimeout, - doubleTapAction: Action? = nil, - repeatTimer: RepeatGestureTimer = .shared, - repeatAction: Action? = nil, - dragStartAction: DragAction? = nil, - dragAction: DragAction? = nil, - dragEndAction: DragAction? = nil, - endAction: Action? = nil, - label: @escaping LabelBuilder - ) { - self.isPressedBinding = isPressed ?? .constant(false) - self._config = State(wrappedValue: GestureConfiguration( - state: GestureState(), - pressAction: pressAction ?? {}, - releaseInsideAction: releaseInsideAction ?? {}, - releaseOutsideAction: releaseOutsideAction ?? {}, - longPressDelay: longPressDelay, - longPressAction: longPressAction ?? {}, - doubleTapTimeout: doubleTapTimeout, - doubleTapAction: doubleTapAction ?? {}, - repeatTimer: repeatTimer, - repeatAction: repeatAction, - dragStartAction: dragStartAction, - dragAction: dragAction, - dragEndAction: dragEndAction, - endAction: endAction ?? {}, - label: label - )) - } - - public typealias Action = () -> Void - public typealias DragAction = (DragGesture.Value) -> Void - public typealias LabelBuilder = (_ isPressed: Bool) -> Label - - var isPressedBinding: Binding - - @State - var config: GestureConfiguration - - @State - private var isPressed = false - - @State - private var isPressedByGesture = false - - public var body: some View { - Button(action: config.releaseInsideAction) { - config.label(isPressed) - .withDragGestureActions( - for: self.config, - isPressed: $isPressed, - isPressedByGesture: $isPressedByGesture - ) - } - .buttonStyle( - Style( - isPressed: $isPressed, - isPressedByGesture: $isPressedByGesture, - config: config - ) - ) - .onChange(of: isPressed) { newValue in - isPressedBinding.wrappedValue = newValue - } - .onChange(of: isPressedByGesture) { newValue in - isPressed = newValue - } - } -} - -public extension HamsterScrollViewGestureButton { - class GestureState: ObservableObject { - @Published - var doubleTapDate = Date() - } - - struct GestureConfiguration { - let state: GestureState - let pressAction: Action - let releaseInsideAction: Action - let releaseOutsideAction: Action - let longPressDelay: TimeInterval - let longPressAction: Action - let doubleTapTimeout: TimeInterval - let doubleTapAction: Action - let repeatTimer: RepeatGestureTimer - let repeatAction: Action? - let dragStartAction: DragAction? - let dragAction: DragAction? - let dragEndAction: DragAction? - let endAction: Action - let label: LabelBuilder - - func tryStartRepeatTimer() { - if repeatTimer.isActive { return } - guard let action = repeatAction else { return } - repeatTimer.start(action: action) - } - - func tryStopRepeatTimer() { - guard repeatTimer.isActive else { return } - repeatTimer.stop() - } - - func tryTriggerDoubleTap() { - let interval = Date().timeIntervalSince(state.doubleTapDate) - let trigger = interval < doubleTapTimeout - state.doubleTapDate = trigger ? .distantPast : Date() - guard trigger else { return } - doubleTapAction() - } - } - - struct Style: ButtonStyle { - var isPressed: Binding - var isPressedByGesture: Binding - var config: GestureConfiguration - - @State - var longPressDate = Date() - - public func makeBody(configuration: Configuration) -> some View { - configuration.label - .onChange(of: configuration.isPressed) { isPressed in - longPressDate = Date() - if isPressed { - handleIsPressed() - } else { - handleIsEnded() - } - } - } - } -} - -private extension HamsterScrollViewGestureButton.Style { - func handleIsPressed() { - isPressed.wrappedValue = true - config.pressAction() - tryTriggerLongPressAfterDelay(triggered: longPressDate) - } - - func handleIsEnded() { - if isPressedByGesture.wrappedValue { return } - isPressed.wrappedValue = false - config.endAction() - } - - func tryTriggerLongPressAfterDelay(triggered date: Date) { - DispatchQueue.main.asyncAfter(deadline: .now() + config.longPressDelay) { - guard date == longPressDate else { return } - config.longPressAction() - } - } -} - -private extension View { - typealias Action = () -> Void - typealias DragAction = (DragGesture.Value) -> Void - - @ViewBuilder - func withDragGestureActions( - for config: HamsterScrollViewGestureButton