diff --git a/.rive_head b/.rive_head index 9e946c4e..3a3da295 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -fb8ecf3552aeece0b1d242804580a016b6297ad4 +7986d64d8371531716ea3f038dcbec5da187e6cd diff --git a/Example-iOS/Assets/fallback_fonts.riv b/Example-iOS/Assets/fallback_fonts.riv index 4d900c52..bfaabf2f 100644 Binary files a/Example-iOS/Assets/fallback_fonts.riv and b/Example-iOS/Assets/fallback_fonts.riv differ diff --git a/Example-iOS/Source/Examples/SwiftUI/SwiftFallbackFonts.swift b/Example-iOS/Source/Examples/SwiftUI/SwiftFallbackFonts.swift index 5e4f8c00..18a1014b 100644 --- a/Example-iOS/Source/Examples/SwiftUI/SwiftFallbackFonts.swift +++ b/Example-iOS/Source/Examples/SwiftUI/SwiftFallbackFonts.swift @@ -12,14 +12,17 @@ import RiveRuntime struct SwiftFallbackFonts: View, DismissableView { var dismiss: () -> Void = {} - @StateObject private var viewModel = RiveViewModel(fileName: "fallback_fonts") + @StateObject private var viewModel = RiveViewModel(fileName: "fallback_fonts", fit: .fill) private var runBinding: Binding { Binding { - return self.viewModel.getTextRunValue("text") ?? "" + return self.viewModel.getTextRunValue("ultralight") ?? "" } set: { text in - try? self.viewModel.setTextRunValue("text", textValue: text) + try? self.viewModel.setTextRunValue("ultralight", textValue: text) + try? self.viewModel.setTextRunValue("regular", textValue: text) + try? self.viewModel.setTextRunValue("bold", textValue: text) + try? self.viewModel.setTextRunValue("black", textValue: text) self.viewModel.play() } } @@ -29,7 +32,7 @@ struct SwiftFallbackFonts: View, DismissableView { viewModel.view().scaledToFit() Text( - "The included Rive font only contains characters in the set A...G. Fallback font(s) will be used to draw missing characters." + "The included Rive font only contains characters in the set A...G. Fallback font(s) will be used to draw missing characters with the correct weight." ) .fixedSize(horizontal: false, vertical: /*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) .font(.caption) @@ -42,6 +45,8 @@ struct SwiftFallbackFonts: View, DismissableView { Spacer().frame(maxHeight: .infinity) } .onAppear { + // Set fallback fonts to be used for all styles. + // If fallbackFontsCallback is set, this is unused. RiveFont.fallbackFonts = [ // You can use a font descriptor that will generate a system font RiveFallbackFontDescriptor(design: .default, weight: .regular, width: .standard), @@ -50,6 +55,29 @@ struct SwiftFallbackFonts: View, DismissableView { // ...or a UIFont by name, or any way of initializing a UIFont UIFont(name: "Times New Roman", size: 12)! ] + // ...or set a callback that returns different fonts based on style. + // If fallbackFonts is set, this is unused. + RiveFont.fallbackFontsCallback = { (style: RiveFontStyle) -> [RiveFallbackFontProvider] in + switch style.weight { + case .ultraLight: return [ + RiveFallbackFontDescriptor(weight: .ultraLight), + UIFont.systemFont(ofSize: 20, weight: .ultraLight) + ] + case .regular: return [ + RiveFallbackFontDescriptor(), + UIFont.systemFont(ofSize: 20, weight: .regular) + ] + case .bold: return [ + RiveFallbackFontDescriptor(weight: .bold), + UIFont.systemFont(ofSize: 20, weight: .bold) + ] + case .black: return [ + RiveFallbackFontDescriptor(weight: .black), + UIFont.systemFont(ofSize: 20, weight: .black) + ] + default: return [RiveFallbackFontDescriptor()] + } + } } } } diff --git a/RiveRuntime.xcodeproj/project.pbxproj b/RiveRuntime.xcodeproj/project.pbxproj index 8ddf6d51..0bc29004 100644 --- a/RiveRuntime.xcodeproj/project.pbxproj +++ b/RiveRuntime.xcodeproj/project.pbxproj @@ -108,6 +108,10 @@ F2D285492C6D469900728340 /* RiveFallbackFontProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D285482C6D469900728340 /* RiveFallbackFontProvider.swift */; }; F2ECC2312C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ECC2302C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift */; }; F2ECC23A2C66B949008B20E5 /* RiveFontTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2ECC2382C66B920008B20E5 /* RiveFontTests.swift */; }; + F2FD94042CC9492B00C1FC85 /* RiveFont.m in Sources */ = {isa = PBXBuildFile; fileRef = F2FD94032CC9492B00C1FC85 /* RiveFont.m */; }; + F2FD94052CC9492B00C1FC85 /* RiveFont.h in Headers */ = {isa = PBXBuildFile; fileRef = F2FD94022CC9492B00C1FC85 /* RiveFont.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F2FD94082CC94B1A00C1FC85 /* RiveFallbackFontCache.m in Sources */ = {isa = PBXBuildFile; fileRef = F2FD94072CC94B1A00C1FC85 /* RiveFallbackFontCache.m */; }; + F2FD94092CC94B1A00C1FC85 /* RiveFallbackFontCache.h in Headers */ = {isa = PBXBuildFile; fileRef = F2FD94062CC94B1A00C1FC85 /* RiveFallbackFontCache.h */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -224,6 +228,10 @@ F2D285482C6D469900728340 /* RiveFallbackFontProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFallbackFontProvider.swift; sourceTree = ""; }; F2ECC2302C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RiveFallbackFontDescriptor+Extensions.swift"; sourceTree = ""; }; F2ECC2382C66B920008B20E5 /* RiveFontTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RiveFontTests.swift; sourceTree = ""; }; + F2FD94022CC9492B00C1FC85 /* RiveFont.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RiveFont.h; sourceTree = ""; }; + F2FD94032CC9492B00C1FC85 /* RiveFont.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveFont.m; sourceTree = ""; }; + F2FD94062CC94B1A00C1FC85 /* RiveFallbackFontCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RiveFallbackFontCache.h; sourceTree = ""; }; + F2FD94072CC94B1A00C1FC85 /* RiveFallbackFontCache.m */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.objcpp; path = RiveFallbackFontCache.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -430,6 +438,10 @@ F21F08132C66526D00FFA205 /* RiveFallbackFontDescriptor.swift */, F2ECC2302C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift */, F2D285482C6D469900728340 /* RiveFallbackFontProvider.swift */, + F2FD94022CC9492B00C1FC85 /* RiveFont.h */, + F2FD94032CC9492B00C1FC85 /* RiveFont.m */, + F2FD94062CC94B1A00C1FC85 /* RiveFallbackFontCache.h */, + F2FD94072CC94B1A00C1FC85 /* RiveFallbackFontCache.m */, ); path = Fonts; sourceTree = ""; @@ -478,8 +490,10 @@ E5964A962A965A9300140479 /* RiveEvent.h in Headers */, 043026022AFB9FCD00320F2E /* RiveFactory.h in Headers */, C9C741F424FC510200EF9516 /* Rive.h in Headers */, + F2FD94052CC9492B00C1FC85 /* RiveFont.h in Headers */, 04BE5436264D2A7500427B39 /* RivePrivateHeaders.h in Headers */, C9C73EE224FC478900EF9516 /* RiveRuntime.h in Headers */, + F2FD94092CC94B1A00C1FC85 /* RiveFallbackFontCache.h in Headers */, 83DE4CA72AAAE72100B88B72 /* RenderContext.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -632,6 +646,7 @@ C3E2B580282F242400A8651B /* RiveStateMachineInstance+Extensions.swift in Sources */, 046FB7F5264EAA60000129B1 /* RiveSMIInput.mm in Sources */, 043026002AFA915B00320F2E /* RiveFileAsset.mm in Sources */, + F2FD94042CC9492B00C1FC85 /* RiveFont.m in Sources */, C3468E5A27ECC7C6008652FD /* RiveViewModel.swift in Sources */, C3745FD3282BFAB90087F4AF /* FPSCounterView.swift in Sources */, F2ECC2312C666824008B20E5 /* RiveFallbackFontDescriptor+Extensions.swift in Sources */, @@ -640,6 +655,7 @@ 046FB7F8264EAA60000129B1 /* RiveStateMachineInstance.mm in Sources */, C3468E5C27ED4C41008652FD /* RiveModel.swift in Sources */, 046FB7FF264EAA61000129B1 /* RiveFile.mm in Sources */, + F2FD94082CC94B1A00C1FC85 /* RiveFallbackFontCache.m in Sources */, 046FB7F2264EAA60000129B1 /* RiveArtboard.mm in Sources */, F2610DD22CA5B4C40090D50B /* RiveLogger+StateMachine.swift in Sources */, 046FB7F4264EAA60000129B1 /* RiveLinearAnimationInstance.mm in Sources */, diff --git a/Source/Fonts/RiveFallbackFontCache.h b/Source/Fonts/RiveFallbackFontCache.h new file mode 100644 index 00000000..1492045f --- /dev/null +++ b/Source/Fonts/RiveFallbackFontCache.h @@ -0,0 +1,44 @@ +// +// RiveFallbackFontCache.h +// RiveRuntime +// +// Created by David Skuza on 10/23/24. +// Copyright © 2024 Rive. All rights reserved. +// + +#import +#import +#import + +@class RiveFontStyle; + +NS_ASSUME_NONNULL_BEGIN + +/// An object that can be used as a dictionary key when caching fallback fonts. +/// - Note: This implements NSCopying and overrides `isEqual` and `hash` to add +/// support for an object of this type to be used as a key in dictionaries. +@interface RiveFallbackFontCacheKey : NSObject +/// The style of the requested fallback font to be cached. +@property(nonatomic, readonly, nonnull) RiveFontStyle* style; +/// The actual character for which a fallback font is being requested. +@property(nonatomic, readonly) rive::Unichar character; +/// The fallback index used when originally requesting a fallback. +@property(nonatomic, readonly) uint32_t index; +- (instancetype)initWithStyle:(RiveFontStyle*)style + character:(rive::Unichar)character + index:(uint32_t)index; +@end + +/// An object that can be used as a dictionary value (typically keyed to +/// `RiveFallbackFontCacheKey`), which contains the cached font types. +@interface RiveFallbackFontCacheValue : NSObject +/// The native font type used as the fallback (passed to the C++ +/// runtime). On iOS, this will be UIFont. On macOS, this +/// will be NSFont. +@property(nonatomic, readonly) id font; +/// Whether the font used the system shaper (i.e Core Text over Harfbuzz) +@property(nonatomic, readonly) BOOL usesSystemShaper; +- (instancetype)initWithFont:(id)font usesSystemShaper:(BOOL)usesSystemShaper; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Fonts/RiveFallbackFontCache.m b/Source/Fonts/RiveFallbackFontCache.m new file mode 100644 index 00000000..2be611b2 --- /dev/null +++ b/Source/Fonts/RiveFallbackFontCache.m @@ -0,0 +1,83 @@ +// +// RiveFallbackFontCache.m +// RiveRuntime +// +// Created by David Skuza on 10/23/24. +// Copyright © 2024 Rive. All rights reserved. +// + +#import "RiveFallbackFontCache.h" +#import "RiveFont.h" + +@implementation RiveFallbackFontCacheKey +@synthesize style = _style; +@synthesize character = _character; +@synthesize index = _index; + +- (instancetype)initWithStyle:(RiveFontStyle*)style + character:(rive::Unichar)character + index:(uint32_t)index +{ + if (self = [super init]) + { + _style = style; + _character = character; + _index = index; + } + return self; +} + +- (BOOL)isEqual:(id)object +{ + if (object == nil) + { + return NO; + } + + if (![object isKindOfClass:[RiveFallbackFontCacheKey class]]) + { + return NO; + } + + if (self == object) + { + return YES; + } + + RiveFallbackFontCacheKey* other = (RiveFallbackFontCacheKey*)object; + return [self.style isEqual:other.style] && + self.character == other.character && self.index == other.index; +} + +- (NSUInteger)hash +{ + // This is a super basic hash function that may be able to be improved. + // However, I don't imagine many collisions will happen based on the + // simplicity of our current use case. - David + return [self.style hash] ^ self.character ^ self.index; +} + +- (id)copyWithZone:(NSZone*)zone +{ + return [[RiveFallbackFontCacheKey alloc] initWithStyle:[self.style copy] + character:self.character + index:self.index]; +} + +@end + +@implementation RiveFallbackFontCacheValue +@synthesize font = _font; +@synthesize usesSystemShaper = _usesSystemShaper; + +- (instancetype)initWithFont:(id)font usesSystemShaper:(BOOL)usesSystemShaper; +{ + if (self = [super init]) + { + _font = font; + _usesSystemShaper = usesSystemShaper; + } + return self; +} + +@end diff --git a/Source/Fonts/RiveFallbackFontDescriptor+Extensions.swift b/Source/Fonts/RiveFallbackFontDescriptor+Extensions.swift index d475be4a..8ad1d799 100644 --- a/Source/Fonts/RiveFallbackFontDescriptor+Extensions.swift +++ b/Source/Fonts/RiveFallbackFontDescriptor+Extensions.swift @@ -185,8 +185,8 @@ extension RiveNativeFont: RiveWeightProvider { // https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping // - David switch weight { - case RiveNativeFontWeight.ultraLight: return 100 - case RiveNativeFontWeight.thin: return 200 + case RiveNativeFontWeight.thin: return 100 + case RiveNativeFontWeight.ultraLight: return 200 case RiveNativeFontWeight.light: return 300 case RiveNativeFontWeight.regular: return 400 case RiveNativeFontWeight.medium: return 500 diff --git a/Source/Fonts/RiveFont.h b/Source/Fonts/RiveFont.h new file mode 100644 index 00000000..17348d3a --- /dev/null +++ b/Source/Fonts/RiveFont.h @@ -0,0 +1,59 @@ +// +// RiveFont.h +// RiveRuntime +// +// Created by David Skuza on 10/23/24. +// Copyright © 2024 Rive. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// An enumeration of possible weight values, mirroring those of +/// UIFont/NSFont.Weight +typedef NS_ENUM(NSInteger, RiveFontStyleWeight) { + RiveFontStyleWeightThin = 100, + RiveFontStyleWeightUltraLight = 200, + RiveFontStyleWeightLight = 300, + RiveFontStyleWeightRegular = 400, + RiveFontStyleWeightMedium = 500, + RiveFontStyleWeightSemibold = 600, + RiveFontStyleWeightBold = 700, + RiveFontStyleWeightHeavy = 800, + RiveFontStyleWeightBlack = 900 +}; + +/// An object that represents the styling of a font. +@interface RiveFontStyle : NSObject +/// The weight of the font. See `RiveFontStyleWeight` for possible values. +/// This value is computed by rounding `rawWeight` to the nearest hundredth. +@property(nonatomic, readonly) RiveFontStyleWeight weight; +/// The raw weight of the font. This value is used to generate the `weight` of +/// the style. +@property(nonatomic, readonly) CGFloat rawWeight; +- (instancetype)initWithWeight:(RiveFontStyleWeight)weight; +- (instancetype)initWithRawWeight:(CGFloat)rawWeight; +@end + +@protocol RiveFallbackFontProvider; + +typedef NSArray>* _Nonnull ( + ^RiveFallbackFontsCallback)(RiveFontStyle*); + +@interface RiveFont : NSObject +/// An array of font descriptors to attempt to use when text being rendererd by +/// Rive uses a font that is missing a glyph. The fonts will be tried in the +/// order in which they are added to the array. +/// - Note: If unset, the default fallback is a default system font, with +/// regular font weight. +@property(class, copy, nonnull) + NSArray>* fallbackFonts; +/// A block that requests fallback font providers, given a font style. +/// This way, different fallback fonts can be used depending on the styling +/// of the font at draw-time (e.g weight). +@property(class, nonatomic, copy, nonnull) + RiveFallbackFontsCallback fallbackFontsCallback; +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Fonts/RiveFont.m b/Source/Fonts/RiveFont.m new file mode 100644 index 00000000..3e2ab01a --- /dev/null +++ b/Source/Fonts/RiveFont.m @@ -0,0 +1,273 @@ +// +// RiveFont.m +// RiveRuntime +// +// Created by David Skuza on 10/23/24. +// Copyright © 2024 Rive. All rights reserved. +// + +#import "RiveFont.h" +#import "RiveFallbackFontCache.h" +#import +#import + +/// Returns a RiveFontStyleWeight for a given float value. Rounds to the nearest +/// hundredth. These values mirror those found here: +/// https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#common_weight_name_mapping +/// - Parameter value: The float value of the weight. +static RiveFontStyleWeight RiveFontStyleWeightFromFloat(float value) +{ + float rounded = round(value / 100); + NSInteger toInt = (NSInteger)rounded * 100; + NSInteger weight = MIN(MAX(toInt, 100), 900); + switch (weight) + { + case RiveFontStyleWeightThin: + return RiveFontStyleWeightThin; + case RiveFontStyleWeightUltraLight: + return RiveFontStyleWeightUltraLight; + case RiveFontStyleWeightLight: + return RiveFontStyleWeightLight; + case RiveFontStyleWeightRegular: + return RiveFontStyleWeightRegular; + case RiveFontStyleWeightMedium: + return RiveFontStyleWeightMedium; + case RiveFontStyleWeightSemibold: + return RiveFontStyleWeightSemibold; + case RiveFontStyleWeightBold: + return RiveFontStyleWeightBold; + case RiveFontStyleWeightHeavy: + return RiveFontStyleWeightHeavy; + case RiveFontStyleWeightBlack: + return RiveFontStyleWeightBlack; + default: + return RiveFontStyleWeightRegular; + } +} + +@implementation RiveFontStyle +- (instancetype)initWithWeight:(RiveFontStyleWeight)weight +{ + if (self = [super init]) + { + _weight = weight; + _rawWeight = CGFloat(weight); + } + return self; +} + +- (instancetype)initWithRawWeight:(CGFloat)rawWeight +{ + if (self = [super init]) + { + _rawWeight = rawWeight; + _weight = RiveFontStyleWeightFromFloat(rawWeight); + } + return self; +} + +- (NSUInteger)hash +{ + return _rawWeight; +} + +// Overwritten since it's used for equality checks of RiveFallbackFontCacheKey +- (BOOL)isEqual:(id)object +{ + if (object == nil) + { + return NO; + } + + if (![object isKindOfClass:[RiveFontStyle class]]) + { + return NO; + } + + if (self == object) + { + return YES; + } + + RiveFontStyle* other = (RiveFontStyle*)object; + return self.weight == other.weight; +} + +- (id)copyWithZone:(NSZone*)zone +{ + return [[RiveFontStyle alloc] initWithWeight:self.weight]; +} +@end + +/// A cache of all used (Rive) fonts, keyed by style and character. +static NSMutableDictionary* _fallbackFontCache = + nil; +/// A user-specified array of fallback fonts. +static NSArray>* _fallbackFonts = nil; +/// A user-specified block that returns usable font providers. +static RiveFallbackFontsCallback _fallbackFontsCallback = nil; + +static rive::rcp riveFontFromNativeFont(id font, + bool useSystemShaper) +{ + uint16_t weight = 400; + if ([font conformsToProtocol:@protocol(RiveWeightProvider)]) + { + weight = [font riveWeightValue]; + } + + uint8_t width = 100; + if ([font conformsToProtocol:@protocol(RiveFontWidthProvider)]) + { + width = [font riveFontWidthValue]; + } + + CTFontRef ctFont = (__bridge CTFontRef)font; + return HBFont::FromSystem((void*)ctFont, useSystemShaper, weight, width); +} + +static rive::rcp findFallbackFont(const rive::Unichar missing, + const uint32_t fallbackIndex, + const rive::Font* font) +{ + // We know font is going to come back as an HBFont + const HBFont* hbFont = static_cast(font); + + // Generate a style that will be used to request a cached font, or otherwise + // user-specified font. + float value = hbFont->getWeight(); + RiveFontStyle* style = [[RiveFontStyle alloc] initWithRawWeight:value]; + + // Using the above style, check the cache keyed by the given style and + // missing character. + RiveFallbackFontCacheKey* cache = + [[RiveFallbackFontCacheKey alloc] initWithStyle:style + character:missing + index:fallbackIndex]; + + // If there is a cached fallback font, use that. + RiveFallbackFontCacheValue* cachedValue = _fallbackFontCache[cache]; + if (cachedValue != nil) + { + auto font = riveFontFromNativeFont(cachedValue.font, + cachedValue.usesSystemShaper); + rive::rcp rcFont = rive::rcp(font); + // because the font was released at load time, we need to give + // it an extra ref whenever we bump it to a reference counted + // pointer. + rcFont->ref(); + return rcFont; + } + + // Otherwise, request possible fallback providers based on the missing + // character and style. fallbackFontsCallback will always be non-nil, + // and use a default array if no explicit callback has been set. + NSArray>* + providers = [RiveFont fallbackFontsCallback](style); + + if (fallbackIndex / 2 < providers.count) + { + id provider = + providers[fallbackIndex % providers.count]; + id fallbackFont = provider.fallbackFont; + BOOL usesSystemShaper = fallbackIndex >= providers.count; + auto font = riveFontFromNativeFont(fallbackFont, usesSystemShaper); + rive::rcp rcFont = rive::rcp(font); + // because the font was released at load time, we need to give + // it an extra ref whenever we bump it to a reference counted + // pointer. + rcFont->ref(); + + // Once we've used a font, cache it for later use. + _fallbackFontCache[cache] = + [[RiveFallbackFontCacheValue alloc] initWithFont:fallbackFont + usesSystemShaper:usesSystemShaper]; + + return rcFont; + } + + return nullptr; +} + +@implementation RiveFont +{ + rive::rcp + instance; // note: we do NOT own this, so don't delete it +} + ++ (void)load +{ + rive::Font::gFallbackProc = findFallbackFont; + _fallbackFontCache = [NSMutableDictionary dictionary]; +} + +- (instancetype)initWithFont:(rive::rcp)font +{ + if (self = [super init]) + { + instance = font; + return self; + } + else + { + return nil; + } +} +- (rive::rcp)instance +{ + return instance; +} + ++ (NSArray>*)fallbackFonts +{ + if (_fallbackFonts.count == 0) + { + return @[ [[RiveFallbackFontDescriptor alloc] + initWithDesign:RiveFallbackFontDescriptorDesignDefault + weight:RiveFallbackFontDescriptorWeightRegular + width:RiveFallbackFontDescriptorWidthStandard] ]; + } + + return _fallbackFonts; +} + ++ (void)setFallbackFonts: + (nonnull NSArray>*)fallbackFonts +{ + // Set the user-specified fallbacks, and reset the cache. + _fallbackFonts = [fallbackFonts copy]; + _fallbackFontCache = [NSMutableDictionary dictionary]; + + // "Reset" fallback fonts callback so that array can take priority + _fallbackFontsCallback = nil; +} + ++ (void)setFallbackFontsCallback:(RiveFallbackFontsCallback)fallbackFontCallback +{ + // Set the user-specified fallback block, and reset the cache. + _fallbackFontsCallback = [fallbackFontCallback copy]; + _fallbackFontCache = [NSMutableDictionary dictionary]; + + // "Reset" fallback fonts array so that callback can take priority + _fallbackFonts = nil; +} + ++ (RiveFallbackFontsCallback)fallbackFontsCallback +{ + // If there is no user-specified block set, use our internal defaults. + if (_fallbackFontsCallback == nil) + { + return ^NSArray>*(RiveFontStyle* style) + { + // Using this getter will always return a font. + // If no user-specified fonts were added, this + // returns a default. + return [RiveFont fallbackFonts]; + }; + } + + return _fallbackFontsCallback; +} + +@end diff --git a/Source/Renderer/RiveFactory.mm b/Source/Renderer/RiveFactory.mm index e248f569..d9ff70f6 100644 --- a/Source/Renderer/RiveFactory.mm +++ b/Source/Renderer/RiveFactory.mm @@ -9,16 +9,14 @@ #import #import #import -#import -#import #import +#import +#import #if TARGET_OS_IPHONE #import #endif -static NSArray>* _fallbackFonts = nil; - static rive::rcp riveFontFromNativeFont(id font, bool useSystemShaper) { @@ -38,75 +36,6 @@ return HBFont::FromSystem((void*)ctFont, useSystemShaper, weight, width); } -static rive::rcp findFallbackFont(const rive::Unichar missing, - const uint32_t fallbackIndex) -{ - // Try all the fallback fonts twice, first pass with harfbuzz and the second - // loop uses the system shaper. - if (fallbackIndex / 2 < RiveFont.fallbackFonts.count) - { - auto f = - [RiveFont - .fallbackFonts[fallbackIndex % RiveFont.fallbackFonts.count] - fallbackFont]; - auto font = riveFontFromNativeFont( - f, fallbackIndex >= RiveFont.fallbackFonts.count); - rive::rcp rcFont = rive::rcp(font); - rcFont->ref(); - return rcFont; - } - return nullptr; -} - -@implementation RiveFont -{ - rive::rcp - instance; // note: we do NOT own this, so don't delete it -} - -+ (void)load -{ - rive::Font::gFallbackProc = findFallbackFont; -} - -- (instancetype)initWithFont:(rive::rcp)font -{ - if (self = [super init]) - { - instance = font; - return self; - } - else - { - return nil; - } -} -- (rive::rcp)instance -{ - return instance; -} - -+ (NSArray>*)fallbackFonts -{ - if (_fallbackFonts.count == 0) - { - return @[ [[RiveFallbackFontDescriptor alloc] - initWithDesign:RiveFallbackFontDescriptorDesignDefault - weight:RiveFallbackFontDescriptorWeightRegular - width:RiveFallbackFontDescriptorWidthStandard] ]; - } - - return _fallbackFonts; -} - -+ (void)setFallbackFonts: - (nonnull NSArray>*)fallbackFonts -{ - _fallbackFonts = [fallbackFonts copy]; -} - -@end - @implementation RiveRenderImage { rive::rcp diff --git a/Source/Renderer/include/Rive.h b/Source/Renderer/include/Rive.h index 28563a9a..f8679c34 100644 --- a/Source/Renderer/include/Rive.h +++ b/Source/Renderer/include/Rive.h @@ -26,6 +26,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/Renderer/include/RiveFactory.h b/Source/Renderer/include/RiveFactory.h index 7658068d..639d22be 100644 --- a/Source/Renderer/include/RiveFactory.h +++ b/Source/Renderer/include/RiveFactory.h @@ -17,19 +17,9 @@ #import #endif -NS_ASSUME_NONNULL_BEGIN - -@protocol RiveFallbackFontProvider; +@class RiveFont; -@interface RiveFont : NSObject -/// An array of font descriptors to attempt to use when text being rendererd by -/// Rive uses a font that is missing a glyph. The fonts will be tried in the -/// order in which they are added to the array. -/// - Note: If unset, the default fallback is a default system font, with -/// regular font weight. -@property(class, copy, nonnull) - NSArray>* fallbackFonts; -@end +NS_ASSUME_NONNULL_BEGIN @interface RiveRenderImage : NSObject @end diff --git a/Tests/RiveFontTests.swift b/Tests/RiveFontTests.swift index 8501a6cf..3a1362ef 100644 --- a/Tests/RiveFontTests.swift +++ b/Tests/RiveFontTests.swift @@ -10,6 +10,10 @@ import XCTest @testable import RiveRuntime class RiveFontTests: XCTestCase { + override func setUp() { + RiveFont.fallbackFonts = [] + } + func testSystemFallbackDefaults() { var defaults = RiveFont.fallbackFonts.compactMap { $0 as? RiveFallbackFontDescriptor } XCTAssertEqual(defaults.first?.design, .default) @@ -26,7 +30,52 @@ class RiveFontTests: XCTestCase { XCTAssertEqual(defaults.first?.design, .default) XCTAssertEqual(defaults.first?.weight, .regular) } - + + func testSystemFallbackCallbackDefaults() { + let style = RiveFontStyle(weight: .regular) + let defaults = RiveFont.fallbackFontsCallback(style).compactMap { $0 as? RiveFallbackFontDescriptor } + XCTAssertEqual(defaults.first?.design, .default) + XCTAssertEqual(defaults.first?.weight, .regular) + } + + func testSystemFallbackCallbackUsesFallbackFontsByDefault() { + RiveFont.fallbackFonts = [RiveFallbackFontDescriptor(weight: .heavy)] + let style = RiveFontStyle(weight: .regular) + let defaults = RiveFont.fallbackFontsCallback(style).compactMap { $0 as? RiveFallbackFontDescriptor } + XCTAssertEqual(defaults.first?.weight, .heavy) + } + + func testSystemFallbackCallbackOverridesFallbackFonts() { + RiveFont.fallbackFonts = [ + RiveFallbackFontDescriptor(weight: .heavy) + ] + + let style = RiveFontStyle(weight: .regular) + var fonts = RiveFont.fallbackFontsCallback(style).compactMap { $0 as? RiveFallbackFontDescriptor } + XCTAssertEqual(fonts.first?.weight, .heavy) + + RiveFont.fallbackFontsCallback = { _ in + return [RiveFallbackFontDescriptor(weight: .thin)] + } + + fonts = RiveFont.fallbackFontsCallback(style).compactMap { $0 as? RiveFallbackFontDescriptor } + XCTAssertEqual(fonts.first?.weight, .thin) + } + + func testSystemFallbackFontsOverridesFallbackCallback() { + RiveFont.fallbackFontsCallback = { _ in + return [RiveFallbackFontDescriptor(weight: .thin)] + } + + RiveFont.fallbackFonts = [ + RiveFallbackFontDescriptor(weight: .regular) + ] + + let style = RiveFontStyle(weight: .regular) + var fonts = RiveFont.fallbackFontsCallback(style).compactMap { $0 as? RiveFallbackFontDescriptor } + XCTAssertEqual(fonts.first?.weight, .regular) + } + func testSystemDesignsReturnFonts() { let defaultDescriptor = RiveFallbackFontDescriptor(design: .default, weight: .regular) let defaultFont = defaultDescriptor.fallbackFont @@ -143,4 +192,51 @@ class RiveFontTests: XCTestCase { XCTAssertNotNil(expandedWidth) XCTAssertLessThan(expandedWidth!, 0) } + + func testRiveFontStyleWeightFromRawWeight() { + // "Normal" weights + var style = RiveFontStyle(rawWeight: 100) + XCTAssertEqual(style.weight, .thin) + + style = RiveFontStyle(rawWeight: 200) + XCTAssertEqual(style.weight, .ultraLight) + + style = RiveFontStyle(rawWeight: 300) + XCTAssertEqual(style.weight, .light) + + style = RiveFontStyle(rawWeight: 400) + XCTAssertEqual(style.weight, .regular) + + style = RiveFontStyle(rawWeight: 500) + XCTAssertEqual(style.weight, .medium) + + style = RiveFontStyle(rawWeight: 600) + XCTAssertEqual(style.weight, .semibold) + + style = RiveFontStyle(rawWeight: 700) + XCTAssertEqual(style.weight, .bold) + + style = RiveFontStyle(rawWeight: 800) + XCTAssertEqual(style.weight, .heavy) + + style = RiveFontStyle(rawWeight: 900) + XCTAssertEqual(style.weight, .black) + + style = RiveFontStyle(rawWeight: -100) + XCTAssertEqual(style.weight, .thin) + + // Outliers + style = RiveFontStyle(rawWeight: 0) + XCTAssertEqual(style.weight, .thin) + + style = RiveFontStyle(rawWeight: 1000) + XCTAssertEqual(style.weight, .black) + + // Rounding + style = RiveFontStyle(rawWeight: 149) + XCTAssertEqual(style.weight, .thin) + + style = RiveFontStyle(rawWeight: 151) + XCTAssertEqual(style.weight, .ultraLight) + } } diff --git a/submodules/rive-runtime b/submodules/rive-runtime index e3e0556f..a629422a 160000 --- a/submodules/rive-runtime +++ b/submodules/rive-runtime @@ -1 +1 @@ -Subproject commit e3e0556f649a3bff8e9c4f31284ebcbecdbea7cb +Subproject commit a629422a34f529d7c713a7499c318a86a29c700a