Button.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /// A control that initiates an action.
  2. public struct Button<Label: View> {
  3. public typealias Content = TupleView1<Label>
  4. /// The label to show on the button.
  5. @_spi(Backends) public var label: () -> Label
  6. /// The action to be performed when the button is clicked.
  7. @_spi(Backends) public var action: @MainActor @Sendable () -> Void
  8. /// Creates a button that displays a text label.
  9. ///
  10. /// - Parameters:
  11. /// - label: The label to show on the button.
  12. /// - action: The action to be performed when the button is clicked.
  13. public init(
  14. _ label: String,
  15. action: @escaping @MainActor @Sendable () -> Void = {}
  16. ) where Label == TupleView1<Text> {
  17. self.label = { TupleView1(Text(label)) }
  18. self.action = action
  19. }
  20. /// Creates a button that displays a custom view as label.
  21. ///
  22. /// - Parameters:
  23. /// - label: The label to show on the button.
  24. /// - action: The action to be performed when the button is clicked.
  25. @MainActor
  26. public init (
  27. action: @escaping @MainActor @Sendable () -> Void = {},
  28. @ViewBuilder label: @escaping @MainActor @Sendable () -> Label
  29. ) {
  30. self.label = label
  31. self.action = action
  32. }
  33. private struct ConstrainedButtonLabel<ConstrainedLabel: View>: View {
  34. @Environment(\.buttonPadding.x) var horizontalPadding
  35. var content: ConstrainedLabel
  36. var width: Int?
  37. var body: some View {
  38. content.ifLet(width) { view, width in
  39. view.frame(width: Double(width - horizontalPadding))
  40. }
  41. }
  42. }
  43. @MainActor
  44. @available(
  45. *,
  46. deprecated,
  47. message: "Use @ViewBuilder init of Button instead and apply a frame modifier to the label."
  48. )
  49. public func _buttonWidth(_ width: Int?) -> Button<some View> {
  50. return Button<TupleView1<ConstrainedButtonLabel<TupleView1<Label>>>>(
  51. action: action,
  52. label: { ConstrainedButtonLabel(content: body, width: width) }
  53. )
  54. }
  55. }
  56. @MainActor
  57. extension Button: TypeSafeView {
  58. public var body: TupleView1<Label> {
  59. label()
  60. }
  61. typealias Children = TupleViewChildren1<Label>
  62. func children<Backend: BaseAppBackend>(
  63. backend: Backend,
  64. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  65. environment: EnvironmentValues
  66. ) -> Children {
  67. Children(label(), backend: backend, snapshots: snapshots, environment: environment)
  68. }
  69. func asWidget<Backend: BaseAppBackend>(
  70. _ children: Children,
  71. backend: Backend
  72. ) -> Backend.Widget {
  73. backend.createButton(wrapping: children.child0.widget.into())
  74. }
  75. func computeLayout<Backend: BaseAppBackend>(
  76. _ widget: Backend.Widget,
  77. children: Children,
  78. proposedSize: ProposedViewSize,
  79. environment: EnvironmentValues,
  80. backend: Backend
  81. ) -> ViewLayoutResult {
  82. let buttonPadding = backend.buttonPadding(in: environment)
  83. let childEnvironment = backend.computeButtonLabelEnvironment(from: environment)
  84. var childProposal = proposedSize
  85. if let proposedWidth = proposedSize.width {
  86. childProposal.width = max(proposedWidth - Double(buttonPadding.x), 0)
  87. }
  88. if let proposedHeight = proposedSize.height {
  89. childProposal.height = max(proposedHeight - Double(buttonPadding.y), 0)
  90. }
  91. let childResult = children.child0.computeLayout(
  92. with: body.view0,
  93. proposedSize: childProposal,
  94. environment: childEnvironment
  95. )
  96. backend.updateButton(
  97. widget,
  98. environment: environment,
  99. action: action
  100. )
  101. // Buttons should always be set to label size + padding.
  102. // The backend representation of a button is expected not to have a minSize.
  103. let size = SIMD2(
  104. Int(childResult.size.width) + buttonPadding.x,
  105. Int(childResult.size.height) + buttonPadding.y
  106. )
  107. return ViewLayoutResult.leafView(size: ViewSize(size))
  108. }
  109. func commit<Backend: BaseAppBackend>(
  110. _ widget: Backend.Widget,
  111. children: Children,
  112. layout: ViewLayoutResult,
  113. environment: EnvironmentValues,
  114. backend: Backend
  115. ) {
  116. _ = children.child0.commit()
  117. backend.setSize(of: widget, to: layout.size.vector)
  118. }
  119. }
  120. @MainActor
  121. extension Button where Label == TupleView1<Text> {
  122. public var _asMenuItems: [MenuItem] {
  123. [.button(self)]
  124. }
  125. }