diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 5f420fcbcf..5ff7ed6b56 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ F84380010000000000000001 /* ProfileSetupRecoveryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84380010000000000000002 /* ProfileSetupRecoveryTests.swift */; }; + DAAB00000000000000000031 /* NativeTablerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAB00000000000000000032 /* NativeTablerTests.swift */; }; BBAA00000000000000000011 /* IdentityFilePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA00000000000000000012 /* IdentityFilePlugin.swift */; }; BBAA00000000000000000001 /* HostedHpkeBackup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA00000000000000000002 /* HostedHpkeBackup.swift */; }; DAAB00000000000000000001 /* NativeTabler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAB00000000000000000002; }; @@ -102,6 +103,7 @@ /* Begin PBXFileReference section */ F84380010000000000000002 /* ProfileSetupRecoveryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupRecoveryTests.swift; sourceTree = ""; }; + DAAB00000000000000000032 /* NativeTablerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTablerTests.swift; sourceTree = ""; }; BBAA00000000000000000012 /* IdentityFilePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityFilePlugin.swift; sourceTree = ""; }; BBAA00000000000000000002 /* HostedHpkeBackup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostedHpkeBackup.swift; sourceTree = ""; }; DAAB00000000000000000002 /* NativeTabler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTabler.swift; sourceTree = ""; }; @@ -209,6 +211,7 @@ children = ( 331C807B294A618700263BE5 /* RunnerTests.swift */, F84380010000000000000002 /* ProfileSetupRecoveryTests.swift */, + DAAB00000000000000000032 /* NativeTablerTests.swift */, AB1200132F40400100A17E01 /* NativeMessageImageLoaderTests.swift */, AB1200132F40400100A17E09 /* IdentityFilePluginTests.swift */, BZZ0000000000000000002C /* BuzzCommunicationNotificationTests.swift */, @@ -619,6 +622,7 @@ files = ( 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, F84380010000000000000001 /* ProfileSetupRecoveryTests.swift in Sources */, + DAAB00000000000000000031 /* NativeTablerTests.swift in Sources */, AB1200032F40400100A17E01 /* NativeMessageImageLoaderTests.swift in Sources */, AB1200032F40400100A17E09 /* IdentityFilePluginTests.swift in Sources */, BZZ0000000000000000002B /* BuzzCommunicationNotificationTests.swift in Sources */, diff --git a/mobile/ios/Runner/NativeTabler.swift b/mobile/ios/Runner/NativeTabler.swift index e4b9c51160..f892b37a2f 100644 --- a/mobile/ios/Runner/NativeTabler.swift +++ b/mobile/ios/Runner/NativeTabler.swift @@ -16,6 +16,15 @@ enum NativeTabler { }() static func image(_ symbol: String, size: CGFloat = 20) -> UIImage? { + // The bundled Tabler font is outline-only. Match filled/circle.svg's + // 10-unit radius on its 24-unit canvas for every filled-circle consumer. + if symbol == "circle.fill" { + return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { _ in + UIColor.black.setFill() + UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: size, height: size) + .insetBy(dx: size / 12, dy: size / 12)).fill() + }.withRenderingMode(.alwaysTemplate) + } guard let code = glyphs[symbol], let scalar = UnicodeScalar(code), let name = fontName, let font = UIFont(name: name, size: size) else { assertionFailure("Missing Tabler icon: \(symbol)") @@ -50,7 +59,6 @@ enum NativeTabler { "crop": 0xea85, // crop "arrow.triangle.2.circlepath.camera": 0xee22, // camera-rotate "circle": 0xea6b, // circle - "circle.fill": 0xea6b, // circle "sun.max": 0xeb30, // sun "moon": 0xeaf8, // moon "ellipsis": 0xea95, // dots diff --git a/mobile/ios/RunnerTests/NativeTablerTests.swift b/mobile/ios/RunnerTests/NativeTablerTests.swift new file mode 100644 index 0000000000..e0537ae5ce --- /dev/null +++ b/mobile/ios/RunnerTests/NativeTablerTests.swift @@ -0,0 +1,27 @@ +import UIKit +import XCTest + +@testable import Buzz + +final class NativeTablerTests: XCTestCase { + @MainActor + func testFilledCircleHasOpaqueCenterAndTransparentCornersAtEachSize() throws { + for size: CGFloat in [16, 20, 32] { + let image = try XCTUnwrap(NativeTabler.image("circle.fill", size: size)) + XCTAssertEqual(image.size, CGSize(width: size, height: size)) + XCTAssertEqual(image.renderingMode, .alwaysTemplate) + let bitmap = try XCTUnwrap(image.cgImage) + var pixels = [UInt8](repeating: 0, count: bitmap.width * bitmap.height * 4) + try pixels.withUnsafeMutableBytes { bytes in + let context = try XCTUnwrap(CGContext(data: bytes.baseAddress, + width: bitmap.width, height: bitmap.height, bitsPerComponent: 8, + bytesPerRow: bitmap.width * 4, space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)) + context.draw(bitmap, in: CGRect(x: 0, y: 0, width: bitmap.width, height: bitmap.height)) + } + let center = ((bitmap.height / 2) * bitmap.width + bitmap.width / 2) * 4 + 3 + XCTAssertEqual(pixels[center], 255, "Filled circle must have an opaque center") + XCTAssertEqual(pixels[3], 0, "The icon canvas must retain transparent corners") + } + } +}