diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 7f416c531c..290502770b 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -641,6 +641,22 @@ public final class AppKitBackend: AppBackend { onChange(textField.stringValue) } textField.onSubmit = onSubmit + + if #available(macOS 14, *) { + textField.contentType = + switch environment.textContentType { + case .url: + .URL + case .phoneNumber: + .telephoneNumber + case .name: + .name + case .emailAddress: + .emailAddress + case .text, .digits(_), .decimal(_): + nil + } + } } public func getContent(ofTextField textField: Widget) -> String { diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index 685b1b43d3..f84c6fdbf3 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -38,6 +38,16 @@ public struct EnvironmentValues { /// The scale factor of the current window. public var windowScaleFactor: Double + /// The type of input that text fields represent. + /// + /// This affects autocomplete suggestions, and on devices with no physical keyboard, which + /// on-screen keyboard to use. + /// + /// Do not use this in place of validation, even if you only plan on supporting mobile + /// devices, as this does not restrict copy-paste and many mobile devices support bluetooth + /// keyboards. + public var textContentType: TextContentType + /// Called by view graph nodes when they resize due to an internal state /// change and end up changing size. Each view graph node sets its own /// handler when passing the environment on to its children, setting up @@ -143,6 +153,7 @@ public struct EnvironmentValues { multilineTextAlignment = .leading colorScheme = .light windowScaleFactor = 1 + textContentType = .text window = nil extraValues = [:] } diff --git a/Sources/SwiftCrossUI/Modifiers/TextContentTypeModifier.swift b/Sources/SwiftCrossUI/Modifiers/TextContentTypeModifier.swift new file mode 100644 index 0000000000..23f4809146 --- /dev/null +++ b/Sources/SwiftCrossUI/Modifiers/TextContentTypeModifier.swift @@ -0,0 +1,11 @@ +extension View { + /// Set the content type of text fields. + /// + /// This controls autocomplete suggestions, and on mobile devices, which on-screen keyboard + /// is shown. + public func textContentType(_ type: TextContentType) -> some View { + EnvironmentModifier(self) { environment in + environment.with(\.textContentType, type) + } + } +} diff --git a/Sources/SwiftCrossUI/TextContentType.swift b/Sources/SwiftCrossUI/TextContentType.swift new file mode 100644 index 0000000000..3323cab959 --- /dev/null +++ b/Sources/SwiftCrossUI/TextContentType.swift @@ -0,0 +1,38 @@ +public enum TextContentType { + /// Plain text. + /// + /// This is the default value. + case text + /// Just digits. + /// + /// For numbers that may include decimals or negative numbers, see ``decimal(signed:)``. + /// + /// If `ascii` is true, the user should only enter the ASCII digits 0-9. If `ascii` is + /// false, on mobile devices they may see a different numeric keypad depending on their + /// locale settings (for example, they may see the digits ० १ २ ३ ४ ५ ६ ७ ८ ९ instead + /// if the language is set to Hindi). + case digits(ascii: Bool) + /// A URL. + /// + /// On mobile devices, this type shows a keyboard with prominent buttons for "/" and ".com", + /// and might not include a spacebar. + case url + /// A phone number. + case phoneNumber + /// A person's name. + /// + /// This typically uses the default keyboard, but informs autocomplete to use contact + /// names rather than regular words. + case name + /// A number. + /// + /// If `signed` is false, on mobile devices it shows a numeric keypad with a decimal point, + /// but not necessarily plus and minus signs. If `signed` is true then more punctuation can + /// be entered. + case decimal(signed: Bool) + /// An email address. + /// + /// This informs autocomplete that the input is an email address, and on mobile devices, + /// displays a keyboard with prominent "@" and "." buttons. + case emailAddress +} diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 100a5002aa..e3260f1e67 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -240,6 +240,36 @@ extension UIKitBackend { textFieldWidget.child.textColor = UIColor(color: environment.suggestedForegroundColor) textFieldWidget.onChange = onChange textFieldWidget.onSubmit = onSubmit + + switch environment.textContentType { + case .text: + textFieldWidget.child.keyboardType = .default + textFieldWidget.child.textContentType = nil + case .digits(ascii: false): + textFieldWidget.child.keyboardType = .numberPad + textFieldWidget.child.textContentType = nil + case .digits(ascii: true): + textFieldWidget.child.keyboardType = .asciiCapableNumberPad + textFieldWidget.child.textContentType = nil + case .url: + textFieldWidget.child.keyboardType = .URL + textFieldWidget.child.textContentType = .URL + case .phoneNumber: + textFieldWidget.child.keyboardType = .phonePad + textFieldWidget.child.textContentType = .telephoneNumber + case .name: + textFieldWidget.child.keyboardType = .namePhonePad + textFieldWidget.child.textContentType = .name + case .decimal(signed: false): + textFieldWidget.child.keyboardType = .decimalPad + textFieldWidget.child.textContentType = nil + case .decimal(signed: true): + textFieldWidget.child.keyboardType = .numbersAndPunctuation + textFieldWidget.child.textContentType = nil + case .emailAddress: + textFieldWidget.child.keyboardType = .emailAddress + textFieldWidget.child.textContentType = .emailAddress + } #if os(iOS) if let updateToolbar = environment.updateToolbar { diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 95bef9079a..de7dd03366 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -801,6 +801,38 @@ public final class WinUIBackend: AppBackend { } missing("text field font handling") + + let inputScope: InputScopeNameValue = + switch environment.textContentType { + case .decimal(_): + .number + case .digits(_): + .digits + case .emailAddress: + .emailSmtpAddress + case .name: + .personalFullName + case .phoneNumber: + .telephoneNumber + case .text: + .default + case .url: + .url + } + + setInputScope(for: textField, to: InputScopeName(inputScope)) + } + + private func setInputScope(for textField: TextBox, to value: InputScopeName) { + if let inputScope = textField.inputScope, + inputScope.names.count == 1 + { + inputScope.names[0] = value + } else { + let inputScope = InputScope() + inputScope.names.append(value) + textField.inputScope = inputScope + } } public func setContent(ofTextField textField: Widget, to content: String) {