diff --git a/GeneratingDocumentationSite b/GeneratingDocumentationSite index fd03976..d14952c 100755 --- a/GeneratingDocumentationSite +++ b/GeneratingDocumentationSite @@ -9,7 +9,7 @@ DOCC_ARCHIVE_PATH="${DOCC_ARCHIVE_PATH:-./$TARGET_NAME.doccarchive}" DOCS_OUTPUT_PATH="${DOCS_OUTPUT_PATH:-./docs}" HOSTING_BASE_PATH="${HOSTING_BASE_PATH:-$TARGET_NAME}" BUNDLE_IDENTIFIER="${BUNDLE_IDENTIFIER:-com.swift-man.$TARGET_NAME}" -BUNDLE_VERSION="${BUNDLE_VERSION:-0.6.0}" +BUNDLE_VERSION="${BUNDLE_VERSION:-0.7.2}" MINIMUM_ACCESS_LEVEL="${MINIMUM_ACCESS_LEVEL:-public}" DOCC="$(xcrun --find docc)" diff --git a/README.md b/README.md index 1eb7a14..bf30efc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SwiftUI-Version ![Badge](https://img.shields.io/badge/swift-white.svg?style=flat-square&logo=Swift) ![Badge](https://img.shields.io/badge/SwiftUI-001b87.svg?style=flat-square&logo=Swift&logoColor=black) -![Badge - Version](https://img.shields.io/badge/Version-0.7.1-1177AA?style=flat-square) +![Badge - Version](https://img.shields.io/badge/Version-0.7.2-1177AA?style=flat-square) ![Badge - Swift Package Manager](https://img.shields.io/badge/SPM-compatible-orange?style=flat-square) ![Badge - Platform](https://img.shields.io/badge/platform-mac_12|ios_15|watchos_8|tvos_15-yellow?style=flat-square) ![Badge - License](https://img.shields.io/badge/license-MIT-black?style=flat-square) @@ -158,6 +158,22 @@ For values updated at high frequency, such as drag gestures or timers, throttle or debounce the bound value at the call site when every intermediate value does not need to be rendered. +## Custom Font Clipping + +Some script or brush fonts draw glyphs outside their measured digit column. Use +`glyphBleed` to give each digit column extra clipping space without changing the +default layout for regular fonts. + +```swift +AnimateNumberText(font: .custom("SignPainter", size: 48), + value: $value, + textColor: $textColor, + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4)) +``` + ## Documentation - [DocC Documentation](https://docs.gorani.me/AnimateNumberText/documentation/animatenumbertext/) @@ -171,6 +187,6 @@ Once you have your Swift package set up, adding AnimateNumberText as a dependenc ```swift dependencies: [ - .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.1") + .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.2") ] ``` diff --git a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md index 74991f6..7781000 100644 --- a/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md +++ b/Sources/AnimateNumberText/AnimateNumberText.docc/AnimateNumberText.md @@ -14,7 +14,7 @@ Add AnimateNumberText to your Swift package dependencies. ```swift dependencies: [ - .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.1") + .package(url: "https://github.com/swift-man/AnimateNumberText.git", from: "0.7.2") ] ``` @@ -169,6 +169,24 @@ For values updated at high frequency, such as drag gestures or timers, throttle or debounce the bound value at the call site when every intermediate value does not need to be rendered. +## Custom Font Clipping + +Some script or brush fonts draw glyphs outside their measured digit column. Use +`glyphBleed` to give each digit column extra clipping space without changing the +default layout for regular fonts. + +```swift +AnimateNumberText( + font: .custom("SignPainter", size: 48), + value: $value, + textColor: $textColor, + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4) +) +``` + ## Topics ### Views diff --git a/Sources/AnimateNumberText/Public/AnimateNumberText.swift b/Sources/AnimateNumberText/Public/AnimateNumberText.swift index a7e7246..56c7878 100644 --- a/Sources/AnimateNumberText/Public/AnimateNumberText.swift +++ b/Sources/AnimateNumberText/Public/AnimateNumberText.swift @@ -24,6 +24,7 @@ public struct AnimateNumberText: View { // MARK: - Animation Properties private let animation: AnimateNumberTextAnimation + private let glyphBleed: EdgeInsets @State private var animationRange: [TextColumn] = [] @@ -46,6 +47,7 @@ public struct AnimateNumberText: View { /// - numberFormatter: An optional formatter for numeric presentation. /// - stringFormatter: An optional string format, such as `"%@ ms"`. /// - animation: The animation configuration used for digit updates. + /// - glyphBleed: Extra clipping space for custom fonts whose glyphs draw outside their column. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public init( font: Font = .largeTitle, @@ -54,13 +56,15 @@ public struct AnimateNumberText: View { textColor: Binding, numberFormatter: NumberFormatter? = nil, stringFormatter: String? = nil, - animation: AnimateNumberTextAnimation = .smooth() + animation: AnimateNumberTextAnimation = .smooth(), + glyphBleed: EdgeInsets = EdgeInsets() ) { self.font = font self.weight = weight self._value = value self._textColor = textColor self.animation = animation + self.glyphBleed = glyphBleed.sanitizedGlyphBleed self.formatter = AnimateNumberTextFormatter(numberFormatter: numberFormatter, stringFormatter: stringFormatter) } @@ -78,6 +82,7 @@ public struct AnimateNumberText: View { /// - numberFormatter: An optional formatter for numeric presentation. /// - stringFormatter: An optional string format, such as `"%@ ms"`. /// - animation: The animation configuration used for digit updates. + /// - glyphBleed: Extra clipping space for custom fonts whose glyphs draw outside their column. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public init( font: Font = .largeTitle, @@ -86,7 +91,8 @@ public struct AnimateNumberText: View { textColor: Color = .primary, numberFormatter: NumberFormatter? = nil, stringFormatter: String? = nil, - animation: AnimateNumberTextAnimation = .smooth() + animation: AnimateNumberTextAnimation = .smooth(), + glyphBleed: EdgeInsets = EdgeInsets() ) { self.init(font: font, weight: weight, @@ -94,7 +100,8 @@ public struct AnimateNumberText: View { textColor: .constant(textColor), numberFormatter: numberFormatter, stringFormatter: stringFormatter, - animation: animation) + animation: animation, + glyphBleed: glyphBleed) } public var body: some View { @@ -185,7 +192,8 @@ public struct AnimateNumberText: View { ReelDigitColumn(position: position, font: font, weight: weight, - textColor: textColor) + textColor: textColor, + glyphBleed: glyphBleed) .animation(animation.digitAnimation(at: ordinal, digitCount: digitCount), value: position) } else { @@ -215,7 +223,7 @@ public struct AnimateNumberText: View { } .offset(y: settingOffset(for: textType, height: size.height)) } - .clipped() + .clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed)) } } @@ -342,6 +350,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { let font: Font let weight: Font.Weight let textColor: Color + let glyphBleed: EdgeInsets var animatableData: Double { get { position } @@ -372,7 +381,7 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { } .offset(y: -(CGFloat(fraction) + 1) * size.height) } - .clipped() + .clipShape(GlyphBleedClipShape(glyphBleed: glyphBleed)) } } @@ -388,3 +397,25 @@ private struct ReelDigitColumn: View, @preconcurrency Animatable { return remainder >= 0 ? remainder : remainder + 10 } } + +extension EdgeInsets { + var sanitizedGlyphBleed: EdgeInsets { + EdgeInsets(top: Swift.max(0, top), + leading: Swift.max(0, leading), + bottom: Swift.max(0, bottom), + trailing: Swift.max(0, trailing)) + } +} + +struct GlyphBleedClipShape: Shape { + let glyphBleed: EdgeInsets + + func path(in rect: CGRect) -> Path { + let expandedRect = CGRect(x: rect.minX - glyphBleed.leading, + y: rect.minY - glyphBleed.top, + width: rect.width + glyphBleed.leading + glyphBleed.trailing, + height: rect.height + glyphBleed.top + glyphBleed.bottom) + + return Path(expandedRect) + } +} diff --git a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift index d9f2729..7b7e045 100644 --- a/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift +++ b/Tests/AnimateNumberTextTests/AnimateNumberTextTests.swift @@ -182,6 +182,53 @@ struct AnimateNumberTextTests { #expect(String(describing: type(of: view)) == "AnimateNumberText") } + @Test + @MainActor + func readOnlyValueInitializerAcceptsGlyphBleed() { + let view = AnimateNumberText(value: 10.23, + glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 2, + trailing: 4)) + + #expect(String(describing: type(of: view)) == "AnimateNumberText") + } + + @Test + func glyphBleedSanitizesNegativeInsets() { + let glyphBleed = EdgeInsets(top: -2, + leading: 4, + bottom: -6, + trailing: 8).sanitizedGlyphBleed + + #expect(glyphBleed.top == 0) + #expect(glyphBleed.leading == 4) + #expect(glyphBleed.bottom == 0) + #expect(glyphBleed.trailing == 8) + } + + @Test + func glyphBleedClipShapeExpandsClipRect() { + let shape = GlyphBleedClipShape(glyphBleed: EdgeInsets(top: 2, + leading: 4, + bottom: 6, + trailing: 8)) + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + + #expect(shape.path(in: rect).boundingRect == CGRect(x: 6, + y: 18, + width: 42, + height: 48)) + } + + @Test + func glyphBleedClipShapeKeepsOriginalRectWithZeroBleed() { + let shape = GlyphBleedClipShape(glyphBleed: EdgeInsets()) + let rect = CGRect(x: 10, y: 20, width: 30, height: 40) + + #expect(shape.path(in: rect).boundingRect == rect) + } + @Test func resizeForAnimationUsesStablePlaceholders() { var columns = [