Shape.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /// A 2D shape that can be drawn as a view.
  2. ///
  3. /// If no stroke color or fill color is specified, the default is no stroke and
  4. /// a fill of the current foreground color.
  5. public protocol Shape: View, Sendable, _RemoveGlobalActorIsolation where Content == EmptyView {
  6. /// Draw the path for this shape.
  7. ///
  8. /// The bounds passed to a shape that is immediately drawn as a view will
  9. /// always have an origin of (0, 0). However, you may pass a different
  10. /// bounding box to subpaths. For example, this code draws a rectangle in
  11. /// the left half of the bounds and an ellipse in the right half:
  12. /// ```swift
  13. /// func path(in bounds: Path.Rect) -> Path {
  14. /// Path()
  15. /// .addSubpath(
  16. /// Rectangle().path(
  17. /// in: Path.Rect(
  18. /// x: bounds.x,
  19. /// y: bounds.y,
  20. /// width: bounds.width / 2.0,
  21. /// height: bounds.height
  22. /// )
  23. /// )
  24. /// )
  25. /// .addSubpath(
  26. /// Ellipse().path(
  27. /// in: Path.Rect(
  28. /// x: bounds.center.x,
  29. /// y: bounds.y,
  30. /// width: bounds.width / 2.0,
  31. /// height: bounds.height
  32. /// )
  33. /// )
  34. /// )
  35. /// }
  36. /// ```
  37. ///
  38. /// - Parameter bounds: The bounds of this shape.
  39. func path(in bounds: Path.Rect) -> Path
  40. /// Determine the ideal size of this shape given the proposed bounds.
  41. ///
  42. /// The default implementation accepts the proposal, replacing unspecified
  43. /// dimensions with 10.
  44. ///
  45. /// - Parameter proposal: The proposed bounds of this shape.
  46. /// - Returns: The shape's size for the given proposal.
  47. func size(fitting proposal: ProposedViewSize) -> ViewSize
  48. }
  49. extension Shape {
  50. public var body: EmptyView { return EmptyView() }
  51. public func size(fitting proposal: ProposedViewSize) -> ViewSize {
  52. proposal.replacingUnspecifiedDimensions(by: ViewSize(10, 10))
  53. }
  54. @MainActor
  55. public func children<Backend: BaseAppBackend>(
  56. backend _: Backend,
  57. snapshots _: [ViewGraphSnapshotter.NodeSnapshot]?,
  58. environment _: EnvironmentValues
  59. ) -> any ViewGraphNodeChildren {
  60. ShapeStorage()
  61. }
  62. @MainActor
  63. public func asWidget<Backend: BaseAppBackend>(
  64. _ children: any ViewGraphNodeChildren,
  65. backend: Backend
  66. ) -> Backend.Widget {
  67. func castBackend_inner<NewBackend: BaseAppBackend & BackendFeatures.Paths>(_ backend: NewBackend) -> NewBackend.Widget {
  68. let container = backend.createPathWidget()
  69. let storage = children as! ShapeStorage
  70. storage.backendPath = backend.createPath()
  71. storage.oldPath = nil
  72. return container
  73. }
  74. guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.Paths) else {
  75. fatalError("Backend does not implement BackendFeatures.Paths")
  76. }
  77. return castBackend_inner(castedBackend) as! Backend.Widget
  78. }
  79. @MainActor
  80. public func computeLayout<Backend: BaseAppBackend>(
  81. _ widget: Backend.Widget,
  82. children: any ViewGraphNodeChildren,
  83. proposedSize: ProposedViewSize,
  84. environment: EnvironmentValues,
  85. backend: Backend
  86. ) -> ViewLayoutResult {
  87. let size = size(fitting: proposedSize)
  88. return ViewLayoutResult.leafView(size: size)
  89. }
  90. @MainActor
  91. public func commit<Backend: BaseAppBackend>(
  92. _ widget: Backend.Widget,
  93. children: any ViewGraphNodeChildren,
  94. layout: ViewLayoutResult,
  95. environment: EnvironmentValues,
  96. backend: Backend
  97. ) {
  98. func castBackend_inner<NewBackend: BaseAppBackend & BackendFeatures.Paths>(_ backend: NewBackend) {
  99. let widget = widget as! NewBackend.Widget
  100. let bounds = Path.Rect(
  101. x: 0.0,
  102. y: 0.0,
  103. width: layout.size.width,
  104. height: layout.size.height
  105. )
  106. let path = path(in: bounds)
  107. let storage = children as! ShapeStorage
  108. let pointsChanged = storage.oldPath?.actions != path.actions
  109. storage.oldPath = path
  110. let backendPath = storage.backendPath as! NewBackend.Path
  111. backend.updatePath(
  112. backendPath,
  113. path,
  114. bounds: bounds,
  115. pointsChanged: pointsChanged,
  116. environment: environment
  117. )
  118. backend.setSize(of: widget, to: layout.size.vector)
  119. backend.renderPath(
  120. backendPath,
  121. container: widget,
  122. strokeColor: Color.clear.resolve(in: environment),
  123. fillColor: environment.suggestedForegroundColor.resolve(in: environment),
  124. overrideStrokeStyle: nil
  125. )
  126. }
  127. guard let castedBackend = backend as? any (BaseAppBackend & BackendFeatures.Paths) else {
  128. fatalError("Backend does not implement BackendFeatures.Paths")
  129. }
  130. castBackend_inner(castedBackend)
  131. }
  132. }
  133. final class ShapeStorage: ViewGraphNodeChildren {
  134. let widgets: [AnyWidget] = []
  135. let erasedNodes: [ErasedViewGraphNode] = []
  136. var backendPath: Any!
  137. var oldPath: Path?
  138. }