RoundedRectangle.swift 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. /// A rounded rectangle.
  2. ///
  3. /// This is not necessarily four line segments and four circular arcs. If
  4. /// possible, this shape uses smoother curves to make the transition between the
  5. /// edges and corners less abrupt.
  6. public struct RoundedRectangle {
  7. public var cornerRadius: Double
  8. /// Creates a ``RoundedRectangle`` instance.
  9. ///
  10. /// - Precondition: `cornerRadius` must be finite and positive.
  11. ///
  12. /// - Parameter cornerRadius: The corner radius for this rounded rectangle.
  13. public init(cornerRadius: Double) {
  14. assert(
  15. cornerRadius >= 0.0 && cornerRadius.isFinite,
  16. "Corner radius must be a positive finite value"
  17. )
  18. self.cornerRadius = cornerRadius
  19. }
  20. /// This shape tries to mimic an order 5 superellipse, extending the sides with line segments.
  21. /// Since paths don't support quintic curves, I'm using an approximation consisting of
  22. /// two cubic curves and a line segment. This constant is the list of control points for
  23. /// the cubic curves. See https://www.desmos.com/calculator/chwx3ddx6u .
  24. ///
  25. /// Preconditions:
  26. /// - points.0 is the same as if a line segment and a circular arc were used
  27. /// - points.6.y == 0.0
  28. fileprivate static let points = (
  29. SIMD2(0.292893218813, 0.292893218813),
  30. SIMD2(0.517, 0.0687864376269),
  31. SIMD2(0.87, 0.0337),
  32. SIMD2(1.13130356636, 0.0139677719414),
  33. SIMD2(1.1973, 0.0089),
  34. SIMD2(1.5038, 0.0002),
  35. SIMD2(1.7, 0.0)
  36. )
  37. /// This corresponds to r_{min} in the above Desmos link. This is the minimum ratio of
  38. /// cornerRadius to half the side length at which the superellipse is not applicable. Above this,
  39. /// line segments and circular arcs are used.
  40. fileprivate static let rMin = 0.441968022436
  41. }
  42. extension RoundedRectangle: Shape {
  43. public func path(in bounds: Path.Rect) -> Path {
  44. // just to avoid `RoundedRectangle.` qualifiers
  45. let rMin = RoundedRectangle.rMin
  46. let points = RoundedRectangle.points
  47. let effectiveRadius = min(cornerRadius, bounds.width / 2.0, bounds.height / 2.0)
  48. let xRatio = effectiveRadius / (bounds.width / 2.0)
  49. let yRatio = effectiveRadius / (bounds.height / 2.0)
  50. // MARK: Early exits
  51. // These code paths are guaranteed to not use the approximations of the quintic curves.
  52. // Optimization: just a circle
  53. if bounds.width == bounds.height && bounds.width <= cornerRadius * 2.0 {
  54. return Circle().path(in: bounds)
  55. }
  56. // Optimization: just a rectangle
  57. if effectiveRadius == 0.0 {
  58. return Rectangle().path(in: bounds)
  59. }
  60. // Optimization: corner radius is too large to use quintic curves
  61. if xRatio >= rMin && yRatio >= rMin {
  62. return Path()
  63. .move(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y))
  64. .addLine(to: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y))
  65. .addArc(
  66. center: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y + effectiveRadius),
  67. radius: effectiveRadius,
  68. startAngle: .pi * 1.5,
  69. endAngle: 0.0,
  70. clockwise: true
  71. )
  72. .addLine(to: SIMD2(x: bounds.maxX, y: bounds.maxY - effectiveRadius))
  73. .addArc(
  74. center: SIMD2(
  75. x: bounds.maxX - effectiveRadius,
  76. y: bounds.maxY - effectiveRadius
  77. ),
  78. radius: effectiveRadius,
  79. startAngle: 0.0,
  80. endAngle: .pi * 0.5,
  81. clockwise: true
  82. )
  83. .addLine(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY))
  84. .addArc(
  85. center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY - effectiveRadius),
  86. radius: effectiveRadius,
  87. startAngle: .pi * 0.5,
  88. endAngle: .pi,
  89. clockwise: true
  90. )
  91. .addLine(to: SIMD2(x: bounds.x, y: bounds.y + effectiveRadius))
  92. .addArc(
  93. center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
  94. radius: effectiveRadius,
  95. startAngle: .pi,
  96. endAngle: .pi * 1.5,
  97. clockwise: true
  98. )
  99. }
  100. return Path()
  101. // MARK: Top edge, right side
  102. .move(to: SIMD2(x: bounds.center.x, y: bounds.y))
  103. .if(xRatio >= rMin) {
  104. $0
  105. .addLine(to: SIMD2(x: bounds.maxX - effectiveRadius, y: bounds.y))
  106. .addArc(
  107. center: SIMD2(
  108. x: bounds.maxX - effectiveRadius,
  109. y: bounds.y + effectiveRadius
  110. ),
  111. radius: effectiveRadius,
  112. startAngle: .pi * 1.5,
  113. endAngle: .pi * 1.75,
  114. clockwise: true
  115. )
  116. } else: {
  117. $0
  118. .addLine(
  119. to: SIMD2(
  120. x: bounds.maxX - points.6.x * effectiveRadius,
  121. y: bounds.y + points.6.y * effectiveRadius
  122. )
  123. )
  124. .addCubicCurve(
  125. control1: SIMD2(
  126. x: bounds.maxX - points.5.x * effectiveRadius,
  127. y: bounds.y + points.5.y * effectiveRadius
  128. ),
  129. control2: SIMD2(
  130. x: bounds.maxX - points.4.x * effectiveRadius,
  131. y: bounds.y + points.4.y * effectiveRadius
  132. ),
  133. to: SIMD2(
  134. x: bounds.maxX - points.3.x * effectiveRadius,
  135. y: bounds.y + points.3.y * effectiveRadius
  136. )
  137. )
  138. .addCubicCurve(
  139. control1: SIMD2(
  140. x: bounds.maxX - points.2.x * effectiveRadius,
  141. y: bounds.y + points.2.y * effectiveRadius
  142. ),
  143. control2: SIMD2(
  144. x: bounds.maxX - points.1.x * effectiveRadius,
  145. y: bounds.y + points.1.y * effectiveRadius
  146. ),
  147. to: SIMD2(
  148. x: bounds.maxX - points.0.x * effectiveRadius,
  149. y: bounds.y + points.0.y * effectiveRadius
  150. )
  151. )
  152. }
  153. // MARK: Right edge
  154. .if(yRatio >= rMin) {
  155. $0
  156. .addArc(
  157. center: SIMD2(
  158. x: bounds.maxX - effectiveRadius,
  159. y: bounds.y + effectiveRadius
  160. ),
  161. radius: effectiveRadius,
  162. startAngle: .pi * 1.75,
  163. endAngle: 0.0,
  164. clockwise: true
  165. )
  166. .addLine(to: SIMD2(x: bounds.maxX, y: bounds.maxY - effectiveRadius))
  167. .addArc(
  168. center: SIMD2(
  169. x: bounds.maxX - effectiveRadius,
  170. y: bounds.maxY - effectiveRadius
  171. ),
  172. radius: effectiveRadius,
  173. startAngle: 0.0,
  174. endAngle: .pi * 0.25,
  175. clockwise: true
  176. )
  177. } else: {
  178. $0
  179. .addCubicCurve(
  180. control1: SIMD2(
  181. x: bounds.maxX - points.1.y * effectiveRadius,
  182. y: bounds.y + points.1.x * effectiveRadius
  183. ),
  184. control2: SIMD2(
  185. x: bounds.maxX - points.2.y * effectiveRadius,
  186. y: bounds.y + points.2.x * effectiveRadius
  187. ),
  188. to: SIMD2(
  189. x: bounds.maxX - points.3.y * effectiveRadius,
  190. y: bounds.y + points.3.x * effectiveRadius
  191. )
  192. )
  193. .addCubicCurve(
  194. control1: SIMD2(
  195. x: bounds.maxX - points.4.y * effectiveRadius,
  196. y: bounds.y + points.4.x * effectiveRadius
  197. ),
  198. control2: SIMD2(
  199. x: bounds.maxX - points.5.y * effectiveRadius,
  200. y: bounds.y + points.5.x * effectiveRadius
  201. ),
  202. to: SIMD2(
  203. x: bounds.maxX - points.6.y * effectiveRadius,
  204. y: bounds.y + points.6.x * effectiveRadius
  205. )
  206. )
  207. .addLine(
  208. to: SIMD2(
  209. x: bounds.maxX - points.6.y * effectiveRadius,
  210. y: bounds.maxY - points.6.x * effectiveRadius
  211. )
  212. )
  213. .addCubicCurve(
  214. control1: SIMD2(
  215. x: bounds.maxX - points.5.y * effectiveRadius,
  216. y: bounds.maxY - points.5.x * effectiveRadius
  217. ),
  218. control2: SIMD2(
  219. x: bounds.maxX - points.4.y * effectiveRadius,
  220. y: bounds.maxY - points.4.x * effectiveRadius
  221. ),
  222. to: SIMD2(
  223. x: bounds.maxX - points.3.y * effectiveRadius,
  224. y: bounds.maxY - points.3.x * effectiveRadius
  225. )
  226. )
  227. .addCubicCurve(
  228. control1: SIMD2(
  229. x: bounds.maxX - points.2.y * effectiveRadius,
  230. y: bounds.maxY - points.2.x * effectiveRadius
  231. ),
  232. control2: SIMD2(
  233. x: bounds.maxX - points.1.y * effectiveRadius,
  234. y: bounds.maxY - points.1.x * effectiveRadius
  235. ),
  236. to: SIMD2(
  237. x: bounds.maxX - points.0.y * effectiveRadius,
  238. y: bounds.maxY - points.0.x * effectiveRadius
  239. )
  240. )
  241. }
  242. // MARK: Bottom edge
  243. .if(xRatio >= rMin) {
  244. $0
  245. .addArc(
  246. center: SIMD2(
  247. x: bounds.maxX - effectiveRadius,
  248. y: bounds.maxY - effectiveRadius
  249. ),
  250. radius: effectiveRadius,
  251. startAngle: .pi * 0.25,
  252. endAngle: .pi * 0.5,
  253. clockwise: true
  254. )
  255. .addLine(to: SIMD2(x: bounds.x + effectiveRadius, y: bounds.maxY))
  256. .addArc(
  257. center: SIMD2(
  258. x: bounds.x + effectiveRadius,
  259. y: bounds.maxY - effectiveRadius
  260. ),
  261. radius: effectiveRadius,
  262. startAngle: .pi * 0.5,
  263. endAngle: .pi * 0.75,
  264. clockwise: true
  265. )
  266. } else: {
  267. $0
  268. .addCubicCurve(
  269. control1: SIMD2(
  270. x: bounds.maxX - points.1.x * effectiveRadius,
  271. y: bounds.maxY - points.1.y * effectiveRadius
  272. ),
  273. control2: SIMD2(
  274. x: bounds.maxX - points.2.x * effectiveRadius,
  275. y: bounds.maxY - points.2.y * effectiveRadius
  276. ),
  277. to: SIMD2(
  278. x: bounds.maxX - points.3.x * effectiveRadius,
  279. y: bounds.maxY - points.3.y * effectiveRadius
  280. )
  281. )
  282. .addCubicCurve(
  283. control1: SIMD2(
  284. x: bounds.maxX - points.4.x * effectiveRadius,
  285. y: bounds.maxY - points.4.y * effectiveRadius
  286. ),
  287. control2: SIMD2(
  288. x: bounds.maxX - points.5.x * effectiveRadius,
  289. y: bounds.maxY - points.5.y * effectiveRadius
  290. ),
  291. to: SIMD2(
  292. x: bounds.maxX - points.6.x * effectiveRadius,
  293. y: bounds.maxY - points.6.y * effectiveRadius
  294. )
  295. )
  296. .addLine(
  297. to: SIMD2(
  298. x: bounds.x + points.6.x * effectiveRadius,
  299. y: bounds.maxY - points.6.y * effectiveRadius
  300. )
  301. )
  302. .addCubicCurve(
  303. control1: SIMD2(
  304. x: bounds.x + points.5.x * effectiveRadius,
  305. y: bounds.maxY - points.5.y * effectiveRadius
  306. ),
  307. control2: SIMD2(
  308. x: bounds.x + points.4.x * effectiveRadius,
  309. y: bounds.maxY - points.4.y * effectiveRadius
  310. ),
  311. to: SIMD2(
  312. x: bounds.x + points.3.x * effectiveRadius,
  313. y: bounds.maxY - points.3.y * effectiveRadius
  314. )
  315. )
  316. .addCubicCurve(
  317. control1: SIMD2(
  318. x: bounds.x + points.2.x * effectiveRadius,
  319. y: bounds.maxY - points.2.y * effectiveRadius
  320. ),
  321. control2: SIMD2(
  322. x: bounds.x + points.1.x * effectiveRadius,
  323. y: bounds.maxY - points.1.y * effectiveRadius
  324. ),
  325. to: SIMD2(
  326. x: bounds.x + points.0.x * effectiveRadius,
  327. y: bounds.maxY - points.0.y * effectiveRadius
  328. )
  329. )
  330. }
  331. // MARK: Left edge
  332. .if(yRatio >= rMin) {
  333. $0
  334. .addArc(
  335. center: SIMD2(
  336. x: bounds.x + effectiveRadius,
  337. y: bounds.maxY - effectiveRadius
  338. ),
  339. radius: effectiveRadius,
  340. startAngle: .pi * 0.75,
  341. endAngle: .pi,
  342. clockwise: true
  343. )
  344. .addLine(to: SIMD2(x: bounds.x, y: bounds.y + effectiveRadius))
  345. .addArc(
  346. center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
  347. radius: effectiveRadius,
  348. startAngle: .pi,
  349. endAngle: .pi * 1.25,
  350. clockwise: true
  351. )
  352. } else: {
  353. $0
  354. .addCubicCurve(
  355. control1: SIMD2(
  356. x: bounds.x + points.1.y * effectiveRadius,
  357. y: bounds.maxY - points.1.x * effectiveRadius
  358. ),
  359. control2: SIMD2(
  360. x: bounds.x + points.2.y * effectiveRadius,
  361. y: bounds.maxY - points.2.x * effectiveRadius
  362. ),
  363. to: SIMD2(
  364. x: bounds.x + points.3.y * effectiveRadius,
  365. y: bounds.maxY - points.3.x * effectiveRadius
  366. )
  367. )
  368. .addCubicCurve(
  369. control1: SIMD2(
  370. x: bounds.x + points.4.y * effectiveRadius,
  371. y: bounds.maxY - points.4.x * effectiveRadius
  372. ),
  373. control2: SIMD2(
  374. x: bounds.x + points.5.y * effectiveRadius,
  375. y: bounds.maxY - points.5.x * effectiveRadius
  376. ),
  377. to: SIMD2(
  378. x: bounds.x + points.6.y * effectiveRadius,
  379. y: bounds.maxY - points.6.x * effectiveRadius
  380. )
  381. )
  382. .addLine(
  383. to: SIMD2(
  384. x: bounds.x + points.6.y * effectiveRadius,
  385. y: bounds.y + points.6.x * effectiveRadius
  386. )
  387. )
  388. .addCubicCurve(
  389. control1: SIMD2(
  390. x: bounds.x + points.5.y * effectiveRadius,
  391. y: bounds.y + points.5.x * effectiveRadius
  392. ),
  393. control2: SIMD2(
  394. x: bounds.x + points.4.y * effectiveRadius,
  395. y: bounds.y + points.4.x * effectiveRadius
  396. ),
  397. to: SIMD2(
  398. x: bounds.x + points.3.y * effectiveRadius,
  399. y: bounds.y + points.3.x * effectiveRadius
  400. )
  401. )
  402. .addCubicCurve(
  403. control1: SIMD2(
  404. x: bounds.x + points.2.y * effectiveRadius,
  405. y: bounds.y + points.2.x * effectiveRadius
  406. ),
  407. control2: SIMD2(
  408. x: bounds.x + points.1.y * effectiveRadius,
  409. y: bounds.y + points.1.x * effectiveRadius
  410. ),
  411. to: SIMD2(
  412. x: bounds.x + points.0.y * effectiveRadius,
  413. y: bounds.y + points.0.x * effectiveRadius
  414. )
  415. )
  416. }
  417. // MARK: Top edge, left side
  418. .if(xRatio >= rMin) {
  419. $0
  420. .addArc(
  421. center: SIMD2(x: bounds.x + effectiveRadius, y: bounds.y + effectiveRadius),
  422. radius: effectiveRadius,
  423. startAngle: .pi * 1.25,
  424. endAngle: .pi * 1.5,
  425. clockwise: true
  426. )
  427. } else: {
  428. $0
  429. .addCubicCurve(
  430. control1: SIMD2(
  431. x: bounds.x + points.1.x * effectiveRadius,
  432. y: bounds.y + points.1.y * effectiveRadius
  433. ),
  434. control2: SIMD2(
  435. x: bounds.x + points.2.x * effectiveRadius,
  436. y: bounds.y + points.2.y * effectiveRadius
  437. ),
  438. to: SIMD2(
  439. x: bounds.x + points.3.x * effectiveRadius,
  440. y: bounds.y + points.3.y * effectiveRadius
  441. )
  442. )
  443. .addCubicCurve(
  444. control1: SIMD2(
  445. x: bounds.x + points.4.x * effectiveRadius,
  446. y: bounds.y + points.4.y * effectiveRadius
  447. ),
  448. control2: SIMD2(
  449. x: bounds.x + points.5.x * effectiveRadius,
  450. y: bounds.y + points.5.y * effectiveRadius
  451. ),
  452. to: SIMD2(
  453. x: bounds.x + points.6.x * effectiveRadius,
  454. y: bounds.y + points.6.y * effectiveRadius
  455. )
  456. )
  457. }
  458. .addLine(to: SIMD2(x: bounds.center.x, y: bounds.y))
  459. }
  460. }
  461. // MARK: InsettableShape
  462. extension RoundedRectangle: InsettableShape {
  463. public func inset(by amount: Double) -> some InsettableShape {
  464. InsetShapeImpl(initialCornerRadius: cornerRadius, insetAmount: amount)
  465. }
  466. struct InsetShapeImpl {
  467. var initialCornerRadius: Double
  468. var insetAmount: Double
  469. }
  470. }
  471. extension RoundedRectangle.InsetShapeImpl: InsettableShape {
  472. private var actualCornerRadius: Double { max(0, initialCornerRadius - insetAmount) }
  473. func path(in bounds: Path.Rect) -> Path {
  474. RoundedRectangle(cornerRadius: actualCornerRadius)
  475. .path(
  476. in: .init(
  477. x: bounds.x + insetAmount,
  478. y: bounds.y + insetAmount,
  479. width: bounds.width - 2 * insetAmount,
  480. height: bounds.height - 2 * insetAmount
  481. )
  482. )
  483. }
  484. func size(fitting proposal: ProposedViewSize) -> ViewSize {
  485. let proposedWidth = proposal.width ?? 10
  486. let proposedHeight = proposal.height ?? 10
  487. return ViewSize(max(proposedWidth, insetAmount * 2), max(proposedHeight, insetAmount * 2))
  488. }
  489. func inset(by amount: Double) -> Self {
  490. Self(initialCornerRadius: initialCornerRadius, insetAmount: insetAmount + amount)
  491. }
  492. }