| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- import Foundation
- /// A view that is scrollable when it would otherwise overflow available space.
- ///
- /// Use the ``View/frame(width:height:alignment:)-(Double?,_,_)`` modifier to
- /// constrain width or height if necessary.
- public struct ScrollView<Content: View>: TypeSafeView, View {
- public var body: VStack<Content>
- public var axes: Axis.Set
- /// Wraps a view in a scrollable container.
- ///
- /// - Parameters:
- /// - axes: The axes of to enable scrolling on. Defaults to
- /// ``Axis/Set/vertical``.
- /// - content: The content of the scroll view.
- public init(_ axes: Axis.Set = .vertical, @ViewBuilder _ content: () -> Content) {
- self.axes = axes
- body = VStack(content: content())
- }
- func children<Backend: BaseAppBackend>(
- backend: Backend,
- snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
- environment: EnvironmentValues
- ) -> ScrollViewChildren<Content> {
- // TODO: Verify that snapshotting works correctly with this
- return ScrollViewChildren(
- wrapping: TupleViewChildren1(
- body,
- backend: backend,
- snapshots: snapshots,
- environment: environment
- ),
- backend: backend
- )
- }
- func layoutableChildren<Backend: BaseAppBackend>(
- backend: Backend,
- children: TupleViewChildren1<VStack<Content>>
- ) -> [LayoutSystem.LayoutableChild] {
- []
- }
- func asWidget<Backend: BaseAppBackend>(
- _ children: ScrollViewChildren<Content>,
- backend: Backend
- ) -> Backend.Widget {
- return backend.createScrollContainer(for: children.innerContainer.into())
- }
- func computeLayout<Backend: BaseAppBackend>(
- _ widget: Backend.Widget,
- children: ScrollViewChildren<Content>,
- proposedSize: ProposedViewSize,
- environment: EnvironmentValues,
- backend: Backend
- ) -> ViewLayoutResult {
- // If all scroll axes are unspecified, then our size is exactly that of
- // the child view. This includes when we have no scroll axes.
- let willEarlyExit = Axis.allCases.allSatisfy({ axis in
- !axes.contains(axis) || proposedSize[component: axis] == nil
- })
- // Probe how big the child would like to be
- var childProposal = proposedSize
- for axis in Axis.allCases where axes.contains(axis) {
- childProposal[component: axis] = nil
- }
- let childResult = children.child.computeLayout(
- with: body,
- proposedSize: childProposal,
- environment: environment.with(
- \.allowLayoutCaching,
- !willEarlyExit || environment.allowLayoutCaching
- )
- )
- if willEarlyExit {
- return childResult
- }
- let contentSize = childResult.size
- // An axis is present when it's a scroll axis AND the corresponding
- // child content size is bigger then the proposed size. If the proposed
- // size along the axis is nil then we don't have a scroll bar.
- let hasHorizontalScrollBar: Bool
- if axes.contains(.horizontal), let proposedWidth = proposedSize.width {
- hasHorizontalScrollBar = contentSize.width > proposedWidth
- } else {
- hasHorizontalScrollBar = false
- }
- children.hasHorizontalScrollBar = hasHorizontalScrollBar
- let hasVerticalScrollBar: Bool
- if axes.contains(.vertical), let proposedHeight = proposedSize.height {
- hasVerticalScrollBar = contentSize.height > proposedHeight
- } else {
- hasVerticalScrollBar = false
- }
- children.hasVerticalScrollBar = hasVerticalScrollBar
- let scrollBarWidth = Double(backend.scrollBarWidth)
- let verticalScrollBarWidth = hasVerticalScrollBar ? scrollBarWidth : 0
- let horizontalScrollBarHeight = hasHorizontalScrollBar ? scrollBarWidth : 0
- // Compute the final size to propose to the child view. Subtract off
- // scroll bar sizes from non-scrolling axes.
- var finalContentSizeProposal = childProposal
- if !axes.contains(.horizontal), let proposedWidth = childProposal.width {
- finalContentSizeProposal.width = proposedWidth - verticalScrollBarWidth
- }
- if !axes.contains(.vertical), let proposedHeight = childProposal.height {
- finalContentSizeProposal.height = proposedHeight - horizontalScrollBarHeight
- }
- // Propose a final size to the child view.
- let finalChildResult = children.child.computeLayout(
- with: nil,
- proposedSize: finalContentSizeProposal,
- environment: environment
- )
- // Compute the outer size.
- var outerSize = finalChildResult.size
- if axes.contains(.horizontal) {
- outerSize.width =
- proposedSize.width
- ?? (finalChildResult.size.width + verticalScrollBarWidth)
- } else {
- outerSize.width += verticalScrollBarWidth
- }
- if axes.contains(.vertical) {
- outerSize.height =
- proposedSize.height
- ?? (finalChildResult.size.height + horizontalScrollBarHeight)
- } else {
- outerSize.height += horizontalScrollBarHeight
- }
- return ViewLayoutResult(
- size: outerSize,
- childResults: [finalChildResult],
- participateInStackLayoutsWhenEmpty: true
- )
- }
- func commit<Backend: BaseAppBackend>(
- _ widget: Backend.Widget,
- children: ScrollViewChildren<Content>,
- layout: ViewLayoutResult,
- environment: EnvironmentValues,
- backend: Backend
- ) {
- let scrollViewSize = layout.size
- let finalContentSize = children.child.commit().size
- backend.setSize(of: widget, to: scrollViewSize.vector)
- backend.setSize(
- of: children.innerContainer.into(),
- to: SIMD2(
- max(finalContentSize.vector.x, scrollViewSize.vector.x),
- max(finalContentSize.vector.y, scrollViewSize.vector.y)
- )
- )
- let contentX: Double
- if finalContentSize.width < scrollViewSize.width {
- let alignment = axes.contains(.vertical)
- ? HorizontalAlignment.center : HorizontalAlignment.leading
- contentX = alignment.position(
- ofChild: finalContentSize.width,
- in: scrollViewSize.width
- )
- } else {
- contentX = 0
- }
- let contentY: Double
- if finalContentSize.height < scrollViewSize.height {
- let alignment = axes.contains(.horizontal)
- ? VerticalAlignment.center : VerticalAlignment.top
- contentY = alignment.position(
- ofChild: finalContentSize.height,
- in: scrollViewSize.height
- )
- } else {
- contentY = 0
- }
- backend.setPosition(
- ofChildAt: 0,
- in: children.innerContainer.into(),
- to: SIMD2(
- LayoutSystem.roundSize(contentX),
- LayoutSystem.roundSize(contentY)
- )
- )
- backend.updateScrollContainer(
- widget,
- environment: environment,
- bounceHorizontally: axes.contains(.horizontal),
- bounceVertically: axes.contains(.vertical),
- hasHorizontalScrollBar: children.hasHorizontalScrollBar,
- hasVerticalScrollBar: children.hasVerticalScrollBar
- )
- }
- }
- class ScrollViewChildren<Content: View>: ViewGraphNodeChildren {
- var children: TupleView1<VStack<Content>>.Children
- var innerContainer: AnyWidget
- var hasVerticalScrollBar = false
- var hasHorizontalScrollBar = false
- var child: AnyViewGraphNode<VStack<Content>> {
- children.child0
- }
- var widgets: [AnyWidget] {
- // The implementation of this property doesn't really matter. It doesn't
- // really have a reason to get used anywhere.
- children.widgets
- }
- var erasedNodes: [ErasedViewGraphNode] {
- children.erasedNodes
- }
- init<Backend: BaseAppBackend>(
- wrapping children: TupleView1<VStack<Content>>.Children,
- backend: Backend
- ) {
- self.children = children
- let innerContainer = backend.createContainer()
- backend.insert(children.child0.widget.into(), into: innerContainer, at: 0)
- self.innerContainer = AnyWidget(innerContainer)
- }
- }
|