SplitView.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import Foundation
  2. /// A two-column split view.
  3. struct SplitView<Sidebar: View, Detail: View>: TypeSafeView, View {
  4. typealias Children = SplitViewChildren<EnvironmentModifier<Sidebar>, Detail>
  5. var body: TupleView2<EnvironmentModifier<Sidebar>, Detail>
  6. /// Creates a two-column split view.
  7. ///
  8. /// - Parameters:
  9. /// - sidebar: The sidebar content.
  10. /// - detail: The detail content.
  11. init(@ViewBuilder sidebar: () -> Sidebar, @ViewBuilder detail: () -> Detail) {
  12. body = TupleView2(
  13. EnvironmentModifier(sidebar()) { $0.with(\.listStyle, .sidebar) },
  14. detail()
  15. )
  16. }
  17. func children<Backend: BaseAppBackend>(
  18. backend: Backend,
  19. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  20. environment: EnvironmentValues
  21. ) -> Children {
  22. SplitViewChildren(
  23. wrapping: body.children(
  24. backend: backend,
  25. snapshots: snapshots,
  26. environment: environment
  27. ),
  28. backend: backend
  29. )
  30. }
  31. func asWidget<Backend: BaseAppBackend>(
  32. _ children: Children,
  33. backend: Backend
  34. ) -> Backend.Widget {
  35. return backend.createSplitView(
  36. leadingChild: children.leadingPaneContainer.into(),
  37. trailingChild: children.trailingPaneContainer.into()
  38. )
  39. }
  40. func computeLayout<Backend: BaseAppBackend>(
  41. _ widget: Backend.Widget,
  42. children: Children,
  43. proposedSize: ProposedViewSize,
  44. environment: EnvironmentValues,
  45. backend: Backend
  46. ) -> ViewLayoutResult {
  47. let leadingWidth = Double(backend.sidebarWidth(ofSplitView: widget))
  48. // TODO: If computeLayout ever becomes a pure requirement of View, then we
  49. // can delay this until commit.
  50. children.minimumLeadingWidth =
  51. children.leadingChild.computeLayout(
  52. with: body.view0,
  53. proposedSize: ProposedViewSize(
  54. 0,
  55. proposedSize.height
  56. ),
  57. environment: environment
  58. ).size.width
  59. children.minimumTrailingWidth =
  60. children.trailingChild.computeLayout(
  61. with: body.view1,
  62. proposedSize: ProposedViewSize(
  63. 0,
  64. proposedSize.height
  65. ),
  66. environment: environment
  67. ).size.width
  68. // TODO: Figure out proper fixedSize behaviour (when width is unspecified)
  69. // Update pane children
  70. let leadingResult = children.leadingChild.computeLayout(
  71. with: body.view0,
  72. proposedSize: ProposedViewSize(
  73. proposedSize.width == nil ? nil : leadingWidth,
  74. proposedSize.height
  75. ),
  76. environment: environment
  77. )
  78. let trailingResult = children.trailingChild.computeLayout(
  79. with: body.view1,
  80. proposedSize: ProposedViewSize(
  81. proposedSize.width.map { $0 - max(leadingWidth, leadingResult.size.width) },
  82. proposedSize.height
  83. ),
  84. environment: environment
  85. )
  86. // Update split view size and sidebar width bounds
  87. let leadingContentSize = leadingResult.size
  88. let trailingContentSize = trailingResult.size
  89. var size = ViewSize(
  90. leadingContentSize.width + trailingContentSize.width,
  91. max(leadingContentSize.height, trailingContentSize.height)
  92. )
  93. if let proposedWidth = proposedSize.width {
  94. size.width = max(size.width, proposedWidth)
  95. }
  96. if let proposedHeight = proposedSize.height {
  97. size.height = max(size.height, proposedHeight)
  98. }
  99. return ViewLayoutResult(
  100. size: size,
  101. childResults: [leadingResult, trailingResult]
  102. )
  103. }
  104. func commit<Backend: BaseAppBackend>(
  105. _ widget: Backend.Widget,
  106. children: Children,
  107. layout: ViewLayoutResult,
  108. environment: EnvironmentValues,
  109. backend: Backend
  110. ) {
  111. backend.setResizeHandler(ofSplitView: widget) {
  112. // The parameter to onResize is currently unused
  113. environment.onResize(.zero)
  114. }
  115. let leadingWidth = backend.sidebarWidth(ofSplitView: widget)
  116. let leadingResult = children.leadingChild.commit()
  117. let trailingResult = children.trailingChild.commit()
  118. backend.setSize(of: widget, to: layout.size.vector)
  119. backend.setSidebarWidthBounds(
  120. ofSplitView: widget,
  121. minimum: LayoutSystem.roundSize(children.minimumLeadingWidth),
  122. maximum: LayoutSystem.roundSize(
  123. max(
  124. children.minimumLeadingWidth,
  125. layout.size.width - children.minimumTrailingWidth
  126. )
  127. )
  128. )
  129. // Center pane children
  130. backend.setPosition(
  131. ofChildAt: 0,
  132. in: children.leadingPaneContainer.into(),
  133. to: SIMD2(
  134. leadingWidth - leadingResult.size.vector.x,
  135. layout.size.vector.y - leadingResult.size.vector.y
  136. ) / 2
  137. )
  138. backend.setPosition(
  139. ofChildAt: 0,
  140. in: children.trailingPaneContainer.into(),
  141. to: SIMD2(
  142. layout.size.vector.x - leadingWidth - trailingResult.size.vector.x,
  143. layout.size.vector.y - trailingResult.size.vector.y
  144. ) / 2
  145. )
  146. }
  147. }
  148. class SplitViewChildren<Sidebar: View, Detail: View>: ViewGraphNodeChildren {
  149. var paneChildren: TupleView2<Sidebar, Detail>.Children
  150. var leadingPaneContainer: AnyWidget
  151. var trailingPaneContainer: AnyWidget
  152. var minimumLeadingWidth: Double
  153. var minimumTrailingWidth: Double
  154. init<Backend: BaseAppBackend>(
  155. wrapping children: TupleView2<Sidebar, Detail>.Children,
  156. backend: Backend
  157. ) {
  158. self.paneChildren = children
  159. let leadingPaneContainer = backend.createContainer()
  160. backend.insert(
  161. paneChildren.child0.widget.into(),
  162. into: leadingPaneContainer,
  163. at: 0
  164. )
  165. let trailingPaneContainer = backend.createContainer()
  166. backend.insert(
  167. paneChildren.child1.widget.into(),
  168. into: trailingPaneContainer,
  169. at: 0
  170. )
  171. self.leadingPaneContainer = AnyWidget(leadingPaneContainer)
  172. self.trailingPaneContainer = AnyWidget(trailingPaneContainer)
  173. self.minimumLeadingWidth = 0
  174. self.minimumTrailingWidth = 0
  175. }
  176. var erasedNodes: [ErasedViewGraphNode] {
  177. paneChildren.erasedNodes
  178. }
  179. var widgets: [AnyWidget] {
  180. [
  181. leadingPaneContainer,
  182. trailingPaneContainer,
  183. ]
  184. }
  185. var leadingChild: AnyViewGraphNode<Sidebar> {
  186. paneChildren.child0
  187. }
  188. var trailingChild: AnyViewGraphNode<Detail> {
  189. paneChildren.child1
  190. }
  191. }