TextField.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. /// A control that displays an editable text interface.
  2. public struct TextField: ElementaryView, View {
  3. /// The ideal width of a `TextField`.
  4. private static let idealWidth: Double = 100
  5. /// The label to show when the field is empty.
  6. private var placeholder: String
  7. /// The field's content.
  8. @Binding private var text: String
  9. /// Creates an editable text field with a given placeholder.
  10. ///
  11. /// - Parameters:
  12. /// - placeholder: The label to show when the field is empty.
  13. /// - text: The field's content.
  14. public init(_ placeholder: String = "", text: Binding<String>) {
  15. self.placeholder = placeholder
  16. self._text = text
  17. }
  18. /// Creates an editable text field with a given placeholder.
  19. @available(*, deprecated, renamed: "init(_:text:)")
  20. public init(_ placeholder: String = "", _ value: Binding<String>? = nil) {
  21. self.placeholder = placeholder
  22. var dummy = ""
  23. self._text = value ?? Binding(get: { dummy }, set: { dummy = $0 })
  24. }
  25. /// Creates an editable text field bound to a binary integer value.
  26. ///
  27. /// The field's content is kept in sync with `value` via simple string
  28. /// conversion. When the user enters text that cannot be parsed as the
  29. /// target integer type, the binding is not updated (the previous value
  30. /// is preserved), mirroring the behaviour of SwiftUI's
  31. /// `TextField(_:value:formatter:)` when the formatter fails.
  32. ///
  33. /// - Parameters:
  34. /// - placeholder: The label to show when the field is empty.
  35. /// - value: A binding to the integer value to edit.
  36. public init<V: BinaryInteger & LosslessStringConvertible>(
  37. _ placeholder: String = "",
  38. value: Binding<V>
  39. ) {
  40. self.placeholder = placeholder
  41. self._text = Binding(
  42. get: { String(value.wrappedValue) },
  43. set: { newString in
  44. if let parsed = V(newString), parsed != value.wrappedValue {
  45. value.wrappedValue = parsed
  46. }
  47. }
  48. )
  49. }
  50. /// Creates an editable text field bound to a binary floating-point value.
  51. ///
  52. /// The field's content is kept in sync with `value` via simple string
  53. /// conversion. When the user enters text that cannot be parsed as the
  54. /// target floating-point type, the binding is not updated (the previous
  55. /// value is preserved), mirroring the behaviour of SwiftUI's
  56. /// `TextField(_:value:formatter:)` when the formatter fails.
  57. ///
  58. /// - Parameters:
  59. /// - placeholder: The label to show when the field is empty.
  60. /// - value: A binding to the floating-point value to edit.
  61. public init<V: BinaryFloatingPoint & LosslessStringConvertible>(
  62. _ placeholder: String = "",
  63. value: Binding<V>
  64. ) {
  65. self.placeholder = placeholder
  66. self._text = Binding(
  67. get: { String(value.wrappedValue) },
  68. set: { newString in
  69. if let parsed = V(newString), parsed != value.wrappedValue {
  70. value.wrappedValue = parsed
  71. }
  72. }
  73. )
  74. }
  75. func asWidget<Backend: BaseAppBackend>(backend: Backend) -> Backend.Widget {
  76. return backend.createTextField()
  77. }
  78. func computeLayout<Backend: BaseAppBackend>(
  79. _ widget: Backend.Widget,
  80. proposedSize: ProposedViewSize,
  81. environment: EnvironmentValues,
  82. backend: Backend
  83. ) -> ViewLayoutResult {
  84. let naturalHeight = backend.naturalSize(of: widget).y
  85. let size = ViewSize(
  86. proposedSize.width ?? Self.idealWidth,
  87. Double(naturalHeight)
  88. )
  89. // TODO: Allow backends to set their own ideal text field width
  90. return ViewLayoutResult.leafView(size: size)
  91. }
  92. func commit<Backend: BaseAppBackend>(
  93. _ widget: Backend.Widget,
  94. layout: ViewLayoutResult,
  95. environment: EnvironmentValues,
  96. backend: Backend
  97. ) {
  98. backend.updateTextField(
  99. widget,
  100. placeholder: placeholder,
  101. environment: environment,
  102. onChange: { newValue in
  103. #if DEBUG
  104. // We perform this check in debug mode to catch backends that cause
  105. // unnecessary binding writes, but avoid doing so in release mode
  106. // because comparing text may often be more expensive than just
  107. // avoiding the additional write at the backend level. These
  108. // additional writes are often the result of the handler being
  109. // triggered when we call backend.setContent(ofTextField:to:)
  110. if self.text == newValue {
  111. logger.warning(
  112. """
  113. Unnecessary write to text Binding of TextField detected, \
  114. please open an issue at \(Meta.issueReportingURL) \
  115. so we can fix it for \(type(of: backend)).
  116. """
  117. )
  118. }
  119. #endif
  120. self.text = newValue
  121. },
  122. onSubmit: environment.onSubmit ?? {}
  123. )
  124. let text = text
  125. if text != backend.getContent(ofTextField: widget) {
  126. backend.setContent(ofTextField: widget, to: text)
  127. }
  128. backend.setSize(of: widget, to: layout.size.vector)
  129. }
  130. }