AspectRatioModifier.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. extension View {
  2. /// Modifies size proposals to match the specified aspect ratio, or the
  3. /// view's ideal aspect ratio if unspecified.
  4. ///
  5. /// This modifier doesn't guarantee that the view's size will maintain a
  6. /// constant aspect ratio, as it only changes the size proposed to the
  7. /// wrapped content.
  8. ///
  9. /// - Parameters:
  10. /// - aspectRatio: The aspect ratio to maintain. Use `nil` to
  11. /// maintain the view's ideal aspect ratio.
  12. /// - contentMode: How the view should fill available space.
  13. /// - Returns: A view with its aspect ratio configured.
  14. public func aspectRatio(_ aspectRatio: Double? = nil, contentMode: ContentMode) -> some View {
  15. AspectRatioView(self, aspectRatio: aspectRatio, contentMode: contentMode)
  16. }
  17. /// Modifies size proposals to match the aspect ratio of the provided size.
  18. ///
  19. /// This modifier doesn't guarantee that the view's size will maintain a
  20. /// constant aspect ratio, as it only changes the size proposed to the
  21. /// wrapped content.
  22. ///
  23. /// - Parameters:
  24. /// - aspectRatio: The aspect ratio to maintain, specified as a
  25. /// size with the desired aspect ratio.
  26. /// - contentMode: How the view should fill available space.
  27. /// - Returns: A view with its aspect ratio configured.
  28. public func aspectRatio(_ aspectRatio: SIMD2<Double>, contentMode: ContentMode) -> some View {
  29. AspectRatioView(
  30. self,
  31. aspectRatio: LayoutSystem.aspectRatio(of: aspectRatio),
  32. contentMode: contentMode
  33. )
  34. }
  35. }
  36. struct AspectRatioView<Child: View>: TypeSafeView {
  37. var body: TupleView1<Child>
  38. var aspectRatio: Double?
  39. var contentMode: ContentMode
  40. init(_ child: Child, aspectRatio: Double?, contentMode: ContentMode) {
  41. body = TupleView1(child)
  42. self.aspectRatio = aspectRatio
  43. self.contentMode = contentMode
  44. }
  45. func children<Backend: BaseAppBackend>(
  46. backend: Backend,
  47. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  48. environment: EnvironmentValues
  49. ) -> TupleViewChildren1<Child> {
  50. body.children(backend: backend, snapshots: snapshots, environment: environment)
  51. }
  52. func asWidget<Backend: BaseAppBackend>(
  53. _ children: TupleViewChildren1<Child>,
  54. backend: Backend
  55. ) -> Backend.Widget {
  56. children.child0.widget.into()
  57. }
  58. func computeLayout<Backend: BaseAppBackend>(
  59. _ widget: Backend.Widget,
  60. children: TupleViewChildren1<Child>,
  61. proposedSize: ProposedViewSize,
  62. environment: EnvironmentValues,
  63. backend: Backend
  64. ) -> ViewLayoutResult {
  65. // We lazily compute the aspect ratio because we don't always need it.
  66. // For example, when both dimensions are specified we pass them unmodified.
  67. func computeAspectRatio() -> Double {
  68. if let aspectRatio {
  69. return aspectRatio == 0 ? 1 : aspectRatio
  70. } else {
  71. let childResult = children.child0.computeLayout(
  72. with: body.view0,
  73. proposedSize: .unspecified,
  74. environment: environment
  75. )
  76. return LayoutSystem.aspectRatio(of: childResult.size)
  77. }
  78. }
  79. let proposedFrameSize: ProposedViewSize
  80. switch (proposedSize.width, proposedSize.height) {
  81. case (.some(let width), .some(let height)):
  82. let evaluatedAspectRatio = computeAspectRatio()
  83. let widthForHeight = LayoutSystem.width(
  84. forHeight: height,
  85. aspectRatio: evaluatedAspectRatio
  86. )
  87. let heightForWidth = LayoutSystem.height(
  88. forWidth: width,
  89. aspectRatio: evaluatedAspectRatio
  90. )
  91. switch contentMode {
  92. case .fill:
  93. proposedFrameSize = ProposedViewSize(
  94. max(width, widthForHeight),
  95. max(height, heightForWidth)
  96. )
  97. case .fit:
  98. proposedFrameSize = ProposedViewSize(
  99. min(width, widthForHeight),
  100. min(height, heightForWidth)
  101. )
  102. }
  103. case (.some(let width), .none):
  104. proposedFrameSize = ProposedViewSize(
  105. width,
  106. LayoutSystem.height(forWidth: width, aspectRatio: computeAspectRatio())
  107. )
  108. case (.none, .some(let height)):
  109. proposedFrameSize = ProposedViewSize(
  110. LayoutSystem.width(forHeight: height, aspectRatio: computeAspectRatio()),
  111. height
  112. )
  113. case (.none, .none):
  114. proposedFrameSize = .unspecified
  115. }
  116. return children.child0.computeLayout(
  117. with: body.view0,
  118. proposedSize: proposedFrameSize,
  119. environment: environment
  120. )
  121. }
  122. func commit<Backend: BaseAppBackend>(
  123. _ widget: Backend.Widget,
  124. children: TupleViewChildren1<Child>,
  125. layout: ViewLayoutResult,
  126. environment: EnvironmentValues,
  127. backend: Backend
  128. ) {
  129. _ = children.child0.commit()
  130. }
  131. }