ScrollView.swift 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import Foundation
  2. /// A view that is scrollable when it would otherwise overflow available space.
  3. ///
  4. /// Use the ``View/frame(width:height:alignment:)-(Double?,_,_)`` modifier to
  5. /// constrain width or height if necessary.
  6. public struct ScrollView<Content: View>: TypeSafeView, View {
  7. public var body: VStack<Content>
  8. public var axes: Axis.Set
  9. /// Wraps a view in a scrollable container.
  10. ///
  11. /// - Parameters:
  12. /// - axes: The axes of to enable scrolling on. Defaults to
  13. /// ``Axis/Set/vertical``.
  14. /// - content: The content of the scroll view.
  15. public init(_ axes: Axis.Set = .vertical, @ViewBuilder _ content: () -> Content) {
  16. self.axes = axes
  17. body = VStack(content: content())
  18. }
  19. func children<Backend: BaseAppBackend>(
  20. backend: Backend,
  21. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  22. environment: EnvironmentValues
  23. ) -> ScrollViewChildren<Content> {
  24. // TODO: Verify that snapshotting works correctly with this
  25. return ScrollViewChildren(
  26. wrapping: TupleViewChildren1(
  27. body,
  28. backend: backend,
  29. snapshots: snapshots,
  30. environment: environment
  31. ),
  32. backend: backend
  33. )
  34. }
  35. func layoutableChildren<Backend: BaseAppBackend>(
  36. backend: Backend,
  37. children: TupleViewChildren1<VStack<Content>>
  38. ) -> [LayoutSystem.LayoutableChild] {
  39. []
  40. }
  41. func asWidget<Backend: BaseAppBackend>(
  42. _ children: ScrollViewChildren<Content>,
  43. backend: Backend
  44. ) -> Backend.Widget {
  45. return backend.createScrollContainer(for: children.innerContainer.into())
  46. }
  47. func computeLayout<Backend: BaseAppBackend>(
  48. _ widget: Backend.Widget,
  49. children: ScrollViewChildren<Content>,
  50. proposedSize: ProposedViewSize,
  51. environment: EnvironmentValues,
  52. backend: Backend
  53. ) -> ViewLayoutResult {
  54. // If all scroll axes are unspecified, then our size is exactly that of
  55. // the child view. This includes when we have no scroll axes.
  56. let willEarlyExit = Axis.allCases.allSatisfy({ axis in
  57. !axes.contains(axis) || proposedSize[component: axis] == nil
  58. })
  59. // Probe how big the child would like to be
  60. var childProposal = proposedSize
  61. for axis in Axis.allCases where axes.contains(axis) {
  62. childProposal[component: axis] = nil
  63. }
  64. let childResult = children.child.computeLayout(
  65. with: body,
  66. proposedSize: childProposal,
  67. environment: environment.with(
  68. \.allowLayoutCaching,
  69. !willEarlyExit || environment.allowLayoutCaching
  70. )
  71. )
  72. if willEarlyExit {
  73. return childResult
  74. }
  75. let contentSize = childResult.size
  76. // An axis is present when it's a scroll axis AND the corresponding
  77. // child content size is bigger then the proposed size. If the proposed
  78. // size along the axis is nil then we don't have a scroll bar.
  79. let hasHorizontalScrollBar: Bool
  80. if axes.contains(.horizontal), let proposedWidth = proposedSize.width {
  81. hasHorizontalScrollBar = contentSize.width > proposedWidth
  82. } else {
  83. hasHorizontalScrollBar = false
  84. }
  85. children.hasHorizontalScrollBar = hasHorizontalScrollBar
  86. let hasVerticalScrollBar: Bool
  87. if axes.contains(.vertical), let proposedHeight = proposedSize.height {
  88. hasVerticalScrollBar = contentSize.height > proposedHeight
  89. } else {
  90. hasVerticalScrollBar = false
  91. }
  92. children.hasVerticalScrollBar = hasVerticalScrollBar
  93. let scrollBarWidth = Double(backend.scrollBarWidth)
  94. let verticalScrollBarWidth = hasVerticalScrollBar ? scrollBarWidth : 0
  95. let horizontalScrollBarHeight = hasHorizontalScrollBar ? scrollBarWidth : 0
  96. // Compute the final size to propose to the child view. Subtract off
  97. // scroll bar sizes from non-scrolling axes.
  98. var finalContentSizeProposal = childProposal
  99. if !axes.contains(.horizontal), let proposedWidth = childProposal.width {
  100. finalContentSizeProposal.width = proposedWidth - verticalScrollBarWidth
  101. }
  102. if !axes.contains(.vertical), let proposedHeight = childProposal.height {
  103. finalContentSizeProposal.height = proposedHeight - horizontalScrollBarHeight
  104. }
  105. // Propose a final size to the child view.
  106. let finalChildResult = children.child.computeLayout(
  107. with: nil,
  108. proposedSize: finalContentSizeProposal,
  109. environment: environment
  110. )
  111. // Compute the outer size.
  112. var outerSize = finalChildResult.size
  113. if axes.contains(.horizontal) {
  114. outerSize.width =
  115. proposedSize.width
  116. ?? (finalChildResult.size.width + verticalScrollBarWidth)
  117. } else {
  118. outerSize.width += verticalScrollBarWidth
  119. }
  120. if axes.contains(.vertical) {
  121. outerSize.height =
  122. proposedSize.height
  123. ?? (finalChildResult.size.height + horizontalScrollBarHeight)
  124. } else {
  125. outerSize.height += horizontalScrollBarHeight
  126. }
  127. return ViewLayoutResult(
  128. size: outerSize,
  129. childResults: [finalChildResult],
  130. participateInStackLayoutsWhenEmpty: true
  131. )
  132. }
  133. func commit<Backend: BaseAppBackend>(
  134. _ widget: Backend.Widget,
  135. children: ScrollViewChildren<Content>,
  136. layout: ViewLayoutResult,
  137. environment: EnvironmentValues,
  138. backend: Backend
  139. ) {
  140. let scrollViewSize = layout.size
  141. let finalContentSize = children.child.commit().size
  142. backend.setSize(of: widget, to: scrollViewSize.vector)
  143. backend.setSize(
  144. of: children.innerContainer.into(),
  145. to: SIMD2(
  146. max(finalContentSize.vector.x, scrollViewSize.vector.x),
  147. max(finalContentSize.vector.y, scrollViewSize.vector.y)
  148. )
  149. )
  150. let contentX: Double
  151. if finalContentSize.width < scrollViewSize.width {
  152. let alignment = axes.contains(.vertical)
  153. ? HorizontalAlignment.center : HorizontalAlignment.leading
  154. contentX = alignment.position(
  155. ofChild: finalContentSize.width,
  156. in: scrollViewSize.width
  157. )
  158. } else {
  159. contentX = 0
  160. }
  161. let contentY: Double
  162. if finalContentSize.height < scrollViewSize.height {
  163. let alignment = axes.contains(.horizontal)
  164. ? VerticalAlignment.center : VerticalAlignment.top
  165. contentY = alignment.position(
  166. ofChild: finalContentSize.height,
  167. in: scrollViewSize.height
  168. )
  169. } else {
  170. contentY = 0
  171. }
  172. backend.setPosition(
  173. ofChildAt: 0,
  174. in: children.innerContainer.into(),
  175. to: SIMD2(
  176. LayoutSystem.roundSize(contentX),
  177. LayoutSystem.roundSize(contentY)
  178. )
  179. )
  180. backend.updateScrollContainer(
  181. widget,
  182. environment: environment,
  183. bounceHorizontally: axes.contains(.horizontal),
  184. bounceVertically: axes.contains(.vertical),
  185. hasHorizontalScrollBar: children.hasHorizontalScrollBar,
  186. hasVerticalScrollBar: children.hasVerticalScrollBar
  187. )
  188. }
  189. }
  190. class ScrollViewChildren<Content: View>: ViewGraphNodeChildren {
  191. var children: TupleView1<VStack<Content>>.Children
  192. var innerContainer: AnyWidget
  193. var hasVerticalScrollBar = false
  194. var hasHorizontalScrollBar = false
  195. var child: AnyViewGraphNode<VStack<Content>> {
  196. children.child0
  197. }
  198. var widgets: [AnyWidget] {
  199. // The implementation of this property doesn't really matter. It doesn't
  200. // really have a reason to get used anywhere.
  201. children.widgets
  202. }
  203. var erasedNodes: [ErasedViewGraphNode] {
  204. children.erasedNodes
  205. }
  206. init<Backend: BaseAppBackend>(
  207. wrapping children: TupleView1<VStack<Content>>.Children,
  208. backend: Backend
  209. ) {
  210. self.children = children
  211. let innerContainer = backend.createContainer()
  212. backend.insert(children.child0.widget.into(), into: innerContainer, at: 0)
  213. self.innerContainer = AnyWidget(innerContainer)
  214. }
  215. }