diff --git a/Sources/TouchpadInputApp/Views/SettingsPanel.swift b/Sources/TouchpadInputApp/Views/SettingsPanel.swift index dde6d5b..f805ced 100644 --- a/Sources/TouchpadInputApp/Views/SettingsPanel.swift +++ b/Sources/TouchpadInputApp/Views/SettingsPanel.swift @@ -17,6 +17,10 @@ struct SettingsPanel: View { value: String(format: "%.2f", session.pressureFloor)) { Slider(value: $session.pressureFloor, in: 0.05...0.50, step: 0.05) } + settingRow(label: "Force-press threshold", + value: String(format: "%.2f", session.forcePressThreshold)) { + Slider(value: $session.forcePressThreshold, in: 0.70...1.00, step: 0.05) + } settingRow(label: "Min contact size", value: String(format: "%.2f", session.minContactSize)) { Slider(value: $session.minContactSize, in: 0.0...1.0, step: 0.05) diff --git a/Sources/TouchpadInputCore/DefaultImplementations/CharacterEmitter.swift b/Sources/TouchpadInputCore/DefaultImplementations/CharacterEmitter.swift index e8d7e6b..9e6b5bb 100644 --- a/Sources/TouchpadInputCore/DefaultImplementations/CharacterEmitter.swift +++ b/Sources/TouchpadInputCore/DefaultImplementations/CharacterEmitter.swift @@ -12,13 +12,14 @@ public struct CharacterEmitter: CharacterResolver { // MARK: CharacterResolver public func character(forZoneID id: String, pressure: Float, - modifiers: Set, pressureFloor: Float) -> Character? { + modifiers: Set, pressureFloor: Float, + forcePressThreshold: Float = 0.95) -> Character? { guard pressure >= pressureFloor else { return nil } guard let zone = grid.zones.first(where: { String($0.character) == id }) else { return nil } if modifiers.contains(.shift) { return Character(String(zone.character).uppercased()) } - return resolve(zone: zone, pressure: pressure) + return resolve(zone: zone, pressure: pressure, forcePressThreshold: forcePressThreshold) } // MARK: Legacy coordinate-based API (kept for direct-use tests) @@ -27,13 +28,13 @@ public struct CharacterEmitter: CharacterResolver { pressure: Float, pressureFloor: Float = 0.30) -> Character? { guard pressure >= pressureFloor else { return nil } guard let zone = grid.zone(at: x, y: y) else { return nil } - return resolve(zone: zone, pressure: pressure) + return resolve(zone: zone, pressure: pressure, forcePressThreshold: 0.95) } // MARK: Private - private func resolve(zone: KeyZone, pressure: Float) -> Character { - if pressure >= 0.95 { + private func resolve(zone: KeyZone, pressure: Float, forcePressThreshold: Float) -> Character { + if pressure >= forcePressThreshold { return zone.altCharacter ?? Character(String(zone.character).uppercased()) } else { return zone.character diff --git a/Sources/TouchpadInputCore/Protocols/CharacterResolver.swift b/Sources/TouchpadInputCore/Protocols/CharacterResolver.swift index a958ae4..546f22c 100644 --- a/Sources/TouchpadInputCore/Protocols/CharacterResolver.swift +++ b/Sources/TouchpadInputCore/Protocols/CharacterResolver.swift @@ -4,5 +4,6 @@ public protocol CharacterResolver: Sendable { /// Returns the character for the given zone and pressure level, or nil if no character should be emitted. func character(forZoneID id: String, pressure: Float, - modifiers: Set, pressureFloor: Float) -> Character? + modifiers: Set, pressureFloor: Float, + forcePressThreshold: Float) -> Character? } diff --git a/Sources/TouchpadInputCore/Session/TouchInputSession.swift b/Sources/TouchpadInputCore/Session/TouchInputSession.swift index 73b3f60..ebdb921 100644 --- a/Sources/TouchpadInputCore/Session/TouchInputSession.swift +++ b/Sources/TouchpadInputCore/Session/TouchInputSession.swift @@ -60,6 +60,7 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve // MARK: Stability settings @Published public var pressureFloor: Float = 0.30 + @Published public var forcePressThreshold: Float = 0.95 @Published public var minContactSize: Float = 0.0 @Published public var zoneCooldownMs: Double = 80.0 @@ -308,7 +309,8 @@ public final class TouchInputSession: ObservableObject, @preconcurrency TouchEve forZoneID: zoneID, pressure: effectivePressure, modifiers: heldModifiers, - pressureFloor: pressureFloor + pressureFloor: pressureFloor, + forcePressThreshold: forcePressThreshold ) { outputBuffer.append(ch) externalOutputTarget?.emit(character: ch)