FrameModifier.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. extension View {
  2. /// Positions this view within an invisible frame having the specified
  3. /// minimum size constraints.
  4. ///
  5. /// - Parameters:
  6. /// - width: The frame's exact width. `nil` lets the view choose its own
  7. /// width instead.
  8. /// - height: The frame's exact height. `nil` lets the view choose its own
  9. /// height instead.
  10. /// - alignment: How to align the view within its container.
  11. @available(*, deprecated, renamed: "frame(width:height:alignment:)")
  12. @_disfavoredOverload
  13. public func frame(
  14. width: Int? = nil,
  15. height: Int? = nil,
  16. alignment: Alignment = .center
  17. ) -> some View {
  18. return frame(
  19. width: width.map(Double.init),
  20. height: height.map(Double.init),
  21. alignment: alignment
  22. )
  23. }
  24. public func frame(
  25. width: Double? = nil,
  26. height: Double? = nil,
  27. alignment: Alignment = .center
  28. ) -> some View {
  29. return StrictFrameView(
  30. self,
  31. width: width,
  32. height: height,
  33. alignment: alignment
  34. )
  35. }
  36. /// Positions this view within an invisible frame having the specified
  37. /// minimum size constraints.
  38. ///
  39. /// - Parameters:
  40. /// - minWidth: The frame's minimum width. `nil` means the frame inherits
  41. /// the minimum width of its content
  42. /// - idealWidth: The frame's ideal width. `nil` lets the frame choose its
  43. /// own ideal width instead.
  44. /// - maxWidth: The frame's maximum width. `nil` means the frame inherits
  45. /// the maximum width of its content
  46. /// - minHeight: The frame's minimum height. `nil` means the frame inherits
  47. /// the minimum height of its content
  48. /// - idealHeight: The frame's ideal height. `nil` lets the frame choose its
  49. /// own ideal height instead.
  50. /// - maxHeight: The frame's maximum height. `nil` means the frame inherits
  51. /// the maximum height of its content
  52. /// - alignment: How to align the view within its container.
  53. @available(
  54. *,
  55. deprecated,
  56. renamed: "frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)"
  57. )
  58. @_disfavoredOverload
  59. public func frame(
  60. minWidth: Int? = nil,
  61. idealWidth: Int? = nil,
  62. maxWidth: Double? = nil,
  63. minHeight: Int? = nil,
  64. idealHeight: Int? = nil,
  65. maxHeight: Double? = nil,
  66. alignment: Alignment = .center
  67. ) -> some View {
  68. return frame(
  69. minWidth: minWidth.map(Double.init),
  70. idealWidth: idealWidth.map(Double.init),
  71. maxWidth: maxWidth,
  72. minHeight: minHeight.map(Double.init),
  73. idealHeight: idealHeight.map(Double.init),
  74. maxHeight: maxHeight,
  75. alignment: alignment
  76. )
  77. }
  78. public func frame(
  79. minWidth: Double? = nil,
  80. idealWidth: Double? = nil,
  81. maxWidth: Double? = nil,
  82. minHeight: Double? = nil,
  83. idealHeight: Double? = nil,
  84. maxHeight: Double? = nil,
  85. alignment: Alignment = .center
  86. ) -> some View {
  87. return FlexibleFrameView(
  88. self,
  89. minWidth: minWidth,
  90. idealWidth: idealWidth,
  91. maxWidth: maxWidth,
  92. minHeight: minHeight,
  93. idealHeight: idealHeight,
  94. maxHeight: maxHeight,
  95. alignment: alignment
  96. )
  97. }
  98. }
  99. /// The implementation for the ``View/frame(width:height:)`` view modifier.
  100. struct StrictFrameView<Child: View>: TypeSafeView {
  101. var body: TupleView1<Child>
  102. /// The exact width to make the view.
  103. var width: Double?
  104. /// The exact height to make the view.
  105. var height: Double?
  106. /// The alignment of the child within the frame.
  107. var alignment: Alignment
  108. /// Wraps a child view with size constraints.
  109. init(_ child: Child, width: Double?, height: Double?, alignment: Alignment) {
  110. body = TupleView1(child)
  111. self.width = width
  112. self.height = height
  113. self.alignment = alignment
  114. }
  115. func children<Backend: BaseAppBackend>(
  116. backend: Backend,
  117. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  118. environment: EnvironmentValues
  119. ) -> TupleViewChildren1<Child> {
  120. body.children(backend: backend, snapshots: snapshots, environment: environment)
  121. }
  122. func asWidget<Backend: BaseAppBackend>(
  123. _ children: TupleViewChildren1<Child>,
  124. backend: Backend
  125. ) -> Backend.Widget {
  126. let container = backend.createContainer()
  127. backend.insert(children.child0.widget.into(), into: container, at: 0)
  128. return container
  129. }
  130. func computeLayout<Backend: BaseAppBackend>(
  131. _ widget: Backend.Widget,
  132. children: TupleViewChildren1<Child>,
  133. proposedSize: ProposedViewSize,
  134. environment: EnvironmentValues,
  135. backend: Backend
  136. ) -> ViewLayoutResult {
  137. let width = width
  138. let height = height
  139. let childResult = children.child0.computeLayout(
  140. with: body.view0,
  141. proposedSize: ProposedViewSize(
  142. width ?? proposedSize.width,
  143. height ?? proposedSize.height
  144. ),
  145. environment: environment
  146. )
  147. let childSize = childResult.size
  148. let frameSize = ViewSize(
  149. width ?? childSize.width,
  150. height ?? childSize.height
  151. )
  152. return ViewLayoutResult(
  153. size: frameSize,
  154. childResults: [childResult]
  155. )
  156. }
  157. func commit<Backend: BaseAppBackend>(
  158. _ widget: Backend.Widget,
  159. children: TupleViewChildren1<Child>,
  160. layout: ViewLayoutResult,
  161. environment: EnvironmentValues,
  162. backend: Backend
  163. ) {
  164. let frameSize = layout.size
  165. let childSize = children.child0.commit().size
  166. let childPosition = alignment.position(
  167. ofChild: childSize.vector,
  168. in: frameSize.vector
  169. )
  170. backend.setSize(of: widget, to: frameSize.vector)
  171. backend.setPosition(ofChildAt: 0, in: widget, to: childPosition)
  172. }
  173. }
  174. /// The implementation for the ``View/frame(width:height:)`` view modifier.
  175. struct FlexibleFrameView<Child: View>: TypeSafeView {
  176. var body: TupleView1<Child>
  177. var minWidth: Double?
  178. var idealWidth: Double?
  179. var maxWidth: Double?
  180. var minHeight: Double?
  181. var idealHeight: Double?
  182. var maxHeight: Double?
  183. /// The alignment of the child within the frame.
  184. var alignment: Alignment
  185. /// Wraps a child view with size constraints.
  186. init(
  187. _ child: Child,
  188. minWidth: Double?,
  189. idealWidth: Double?,
  190. maxWidth: Double?,
  191. minHeight: Double?,
  192. idealHeight: Double?,
  193. maxHeight: Double?,
  194. alignment: Alignment
  195. ) {
  196. self.body = TupleView1(child)
  197. self.minWidth = minWidth
  198. self.minHeight = minHeight
  199. self.idealWidth = idealWidth
  200. self.idealHeight = idealHeight
  201. self.maxWidth = maxWidth
  202. self.maxHeight = maxHeight
  203. self.alignment = alignment
  204. }
  205. func children<Backend: BaseAppBackend>(
  206. backend: Backend,
  207. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  208. environment: EnvironmentValues
  209. ) -> TupleViewChildren1<Child> {
  210. body.children(backend: backend, snapshots: snapshots, environment: environment)
  211. }
  212. func asWidget<Backend: BaseAppBackend>(
  213. _ children: TupleViewChildren1<Child>,
  214. backend: Backend
  215. ) -> Backend.Widget {
  216. let container = backend.createContainer()
  217. backend.insert(children.child0.widget.into(), into: container, at: 0)
  218. return container
  219. }
  220. func clampSize(_ size: ViewSize) -> ViewSize {
  221. var size = size
  222. size.width = clampWidth(size.width)
  223. size.height = clampHeight(size.height)
  224. return size
  225. }
  226. func clampHeight(_ height: Double) -> Double {
  227. LayoutSystem.clamp(
  228. height,
  229. minimum: minHeight,
  230. maximum: maxHeight
  231. )
  232. }
  233. func clampWidth(_ width: Double) -> Double {
  234. LayoutSystem.clamp(
  235. width,
  236. minimum: minWidth,
  237. maximum: maxWidth
  238. )
  239. }
  240. func computeLayout<Backend: BaseAppBackend>(
  241. _ widget: Backend.Widget,
  242. children: TupleViewChildren1<Child>,
  243. proposedSize: ProposedViewSize,
  244. environment: EnvironmentValues,
  245. backend: Backend
  246. ) -> ViewLayoutResult {
  247. var proposedFrameSize = proposedSize
  248. if let proposedWidth = proposedSize.width {
  249. proposedFrameSize.width = clampWidth(proposedWidth)
  250. }
  251. if let proposedHeight = proposedSize.height {
  252. proposedFrameSize.height = clampHeight(proposedHeight)
  253. }
  254. if let idealWidth, proposedSize.width == nil {
  255. proposedFrameSize.width = idealWidth
  256. }
  257. if let idealHeight, proposedSize.height == nil {
  258. proposedFrameSize.height = idealHeight
  259. }
  260. let childResult = children.child0.computeLayout(
  261. with: body.view0,
  262. proposedSize: proposedFrameSize,
  263. environment: environment
  264. )
  265. let childSize = childResult.size
  266. var frameSize = clampSize(childSize)
  267. if maxWidth == .infinity, let proposedWidth = proposedSize.width {
  268. frameSize.width = max(frameSize.width, proposedWidth)
  269. }
  270. if maxHeight == .infinity, let proposedHeight = proposedSize.height {
  271. frameSize.height = max(frameSize.height, proposedHeight)
  272. }
  273. return ViewLayoutResult(
  274. size: frameSize,
  275. childResults: [childResult]
  276. )
  277. }
  278. func commit<Backend: BaseAppBackend>(
  279. _ widget: Backend.Widget,
  280. children: TupleViewChildren1<Child>,
  281. layout: ViewLayoutResult,
  282. environment: EnvironmentValues,
  283. backend: Backend
  284. ) {
  285. let frameSize = layout.size
  286. // If the child view has at least one unspecified axis which this frame
  287. // is constraining with a minimum or maximum, then compute its
  288. // layout again with the clamped frame size. This allows the view to
  289. // fill the space that the frame is going to take up anyway. E.g. consider,
  290. //
  291. // ScrollView {
  292. // Color.blue
  293. // .frame(minHeight: 100)
  294. // }
  295. //
  296. // Without this second layout computation, the blue rectangle would
  297. // take on its ideal size of 10 within a frame of height 100, instead
  298. // of using up the min height of the frame as developers may expect.
  299. //
  300. // This doesn't apply to unconstrained axes which we have a corresponding
  301. // ideal length for.
  302. let widthConstrained = minWidth != nil || maxWidth != nil
  303. let heightConstrained = minHeight != nil || maxHeight != nil
  304. let proposedFrameSize = children.child0.lastProposedSize
  305. if (proposedFrameSize.width == nil && widthConstrained)
  306. || (proposedFrameSize.height == nil && heightConstrained)
  307. {
  308. _ = children.child0.computeLayout(
  309. with: nil,
  310. proposedSize: ProposedViewSize(frameSize),
  311. environment: environment
  312. )
  313. }
  314. let childSize = children.child0.commit().size
  315. let childPosition = alignment.position(
  316. ofChild: childSize.vector,
  317. in: frameSize.vector
  318. )
  319. backend.setSize(of: widget, to: frameSize.vector)
  320. backend.setPosition(ofChildAt: 0, in: widget, to: childPosition)
  321. }
  322. }