diff --git a/apps/docs/docs/text/font-style-set.md b/apps/docs/docs/text/font-style-set.md new file mode 100644 index 0000000000..6c6f4445f0 --- /dev/null +++ b/apps/docs/docs/text/font-style-set.md @@ -0,0 +1,126 @@ +--- +id: font-style-set +title: Font Style Sets +sidebar_label: Font Style Sets +slug: /text/font-style-set +--- + +`SkFontMgr` can enumerate the individual variants (regular, italic, bold, +condensed, etc.) that exist within a single font family. This is the lower-level +API that sits behind `matchFamilyStyle` and is useful when you need to: + +- List every variant a family ships with (e.g. to build a weight/width picker). +- Pick the closest available variant to a target style and know exactly which + one you got (name, weight, width, slant). +- Avoid the silent fallback that `matchFamilyStyle` performs when a family is + missing — `matchFamily` returns `null` instead. + +## `SkFontMgr.matchFamily` + +```ts +matchFamily(name: string): SkFontStyleSet | null +``` + +Returns the set of variants registered for the named family, or `null` if the +family is not known. Family aliases such as `"System"` on iOS are resolved the +same way as `matchFamilyStyle`. + +```tsx +import { Skia } from '@shopify/react-native-skia'; + +const styleSet = Skia.FontMgr.System().matchFamily('Helvetica'); +if (styleSet) { + console.log(`Helvetica ships ${styleSet.count()} variants`); +} +``` + +## `SkFontMgr.createStyleSet` + +```ts +createStyleSet(index: number): SkFontStyleSet | null +``` + +Returns the style set for the family at the given family-index (0 through +`countFamilies() - 1`), or `null` if the index is out of range. + +```tsx +const mgr = Skia.FontMgr.System(); +for (let i = 0; i < mgr.countFamilies(); i++) { + const name = mgr.getFamilyName(i); + const set = mgr.createStyleSet(i); + console.log(name, set?.count()); +} +``` + +## `SkFontStyleSet` + +An enumerable set of font variants for a single family. It is an opaque host +object; call `dispose()` when you are done with it. + +| Method | Description | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `count(): number` | Number of variants in the set. | +| `getStyle(index: number): FontStyleEntry` | Style metadata for the variant at `index`. Throws if out of range. | +| `createTypeface(index: number): SkTypeface \| null` | Loads the typeface for the variant at `index`. Returns `null` if the system cannot produce it. | +| `matchStyle(style: FontStyle): SkTypeface \| null` | Returns the typeface whose style most closely matches `style`. Unlike `SkFontMgr.matchFamilyStyle`, this does not fall back across families. | +| `dispose(): void` | Releases the underlying native object. | + +### `FontStyleEntry` + +```ts +interface FontStyleEntry extends Required { + name: string; +} +``` + +- `weight`: one of `FontWeight` (100–1000). Custom fonts may use values outside + the named constants. +- `width`: one of `FontWidth` (1–9). +- `slant`: one of `FontSlant` (`Upright`, `Italic`, `Oblique`). +- `name`: the variant's PostScript name (e.g. `"Helvetica-BoldOblique"`). + +## Example: pick the closest variant in a family + +```tsx +import { + Skia, + FontSlant, + FontWeight, + FontWidth, + type FontStyleEntry, +} from '@shopify/react-native-skia'; + +const target = { + weight: FontWeight.Medium, + width: FontWidth.Normal, + slant: FontSlant.Upright, +}; + +function pickClosest(family: string): FontStyleEntry | null { + const set = Skia.FontMgr.System().matchFamily(family); + if (!set || set.count() === 0) return null; + + let best: FontStyleEntry | null = null; + let bestScore = Infinity; + for (let i = 0; i < set.count(); i++) { + const entry = set.getStyle(i); + const score = + Math.abs(entry.weight - target.weight) * 100 + + Math.abs(entry.width - target.width) * 10 + + (entry.slant === target.slant ? 0 : 1); + if (score < bestScore) { + best = entry; + bestScore = score; + } + } + set.dispose(); + return best; +} +``` + +## Web support + +`matchFamily` and `createStyleSet` are not currently supported on the web +renderer — CanvasKit does not expose the underlying `SkFontStyleSet` API. Both +methods will throw `"Not implemented on React Native Web"` when called in a web +build. diff --git a/packages/skia/cpp/api/JsiSkFontMgr.h b/packages/skia/cpp/api/JsiSkFontMgr.h index aec7f266a5..8e548e09aa 100644 --- a/packages/skia/cpp/api/JsiSkFontMgr.h +++ b/packages/skia/cpp/api/JsiSkFontMgr.h @@ -6,6 +6,7 @@ #include #include "JsiSkFontStyle.h" +#include "JsiSkFontStyleSet.h" #include "JsiSkHostObjects.h" #include "RNSkLog.h" #include @@ -69,13 +70,34 @@ class JsiSkFontMgr : public JsiSkWrappingSkPtrHostObject { runtime, hostObjectInstance, getContext()); } + JSI_HOST_FUNCTION(createStyleSet) { + auto index = static_cast(arguments[0].asNumber()); + auto styleSet = getObject()->createStyleSet(index); + if (!styleSet) { + return jsi::Value::null(); + } + return JsiSkFontStyleSet::toValue(runtime, getContext(), std::move(styleSet)); + } + + JSI_HOST_FUNCTION(matchFamily) { + auto name = arguments[0].asString(runtime).utf8(runtime); + auto resolvedName = getContext()->resolveFontFamily(name); + auto styleSet = getObject()->matchFamily(resolvedName.c_str()); + if (!styleSet) { + return jsi::Value::null(); + } + return JsiSkFontStyleSet::toValue(runtime, getContext(), std::move(styleSet)); + } + size_t getMemoryPressure() const override { return 2048; } std::string getObjectType() const override { return "JsiSkFontMgr"; } JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFontMgr, countFamilies), JSI_EXPORT_FUNC(JsiSkFontMgr, getFamilyName), - JSI_EXPORT_FUNC(JsiSkFontMgr, matchFamilyStyle)) + JSI_EXPORT_FUNC(JsiSkFontMgr, matchFamilyStyle), + JSI_EXPORT_FUNC(JsiSkFontMgr, createStyleSet), + JSI_EXPORT_FUNC(JsiSkFontMgr, matchFamily)) private: std::vector _systemFontFamilies; diff --git a/packages/skia/cpp/api/JsiSkFontStyleSet.h b/packages/skia/cpp/api/JsiSkFontStyleSet.h new file mode 100644 index 0000000000..082a5fb87c --- /dev/null +++ b/packages/skia/cpp/api/JsiSkFontStyleSet.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#include + +#include "JsiSkFontStyle.h" +#include "JsiSkHostObjects.h" +#include "JsiSkTypeface.h" +#include "RNSkLog.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" + +#include "include/core/SkFontMgr.h" +#include "include/core/SkFontStyle.h" +#include "include/core/SkString.h" + +#pragma clang diagnostic pop + +namespace RNSkia { + +namespace jsi = facebook::jsi; + +class JsiSkFontStyleSet : public JsiSkWrappingSkPtrHostObject { +public: + EXPORT_JSI_API_TYPENAME(JsiSkFontStyleSet, FontStyleSet) + + JsiSkFontStyleSet(std::shared_ptr context, + sk_sp fontStyleSet) + : JsiSkWrappingSkPtrHostObject(std::move(context), + std::move(fontStyleSet)) {} + + static jsi::Value toValue(jsi::Runtime &runtime, + std::shared_ptr context, + sk_sp styleSet) { + auto hostObjectInstance = + std::make_shared(context, std::move(styleSet)); + return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE( + runtime, hostObjectInstance, context); + } + + JSI_HOST_FUNCTION(count) { return getObject()->count(); } + + JSI_HOST_FUNCTION(getStyle) { + auto index = static_cast(arguments[0].asNumber()); + auto styleCount = getObject()->count(); + if (index < 0 || index >= styleCount) { + throw jsi::JSError(runtime, "FontStyleSet.getStyle: index out of bounds"); + } + SkFontStyle style; + SkString name; + getObject()->getStyle(index, &style, &name); + auto result = jsi::Object(runtime); + result.setProperty(runtime, "weight", jsi::Value(style.weight())); + result.setProperty(runtime, "width", jsi::Value(style.width())); + result.setProperty(runtime, "slant", + jsi::Value(static_cast(style.slant()))); + result.setProperty(runtime, "name", + jsi::String::createFromUtf8(runtime, name.c_str())); + return result; + } + + JSI_HOST_FUNCTION(createTypeface) { + auto index = static_cast(arguments[0].asNumber()); + auto typeface = getObject()->createTypeface(index); + if (!typeface) { + return jsi::Value::null(); + } + return JsiSkTypeface::toValue(runtime, getContext(), std::move(typeface)); + } + + JSI_HOST_FUNCTION(matchStyle) { + auto fontStyle = JsiSkFontStyle::fromValue(runtime, arguments[0]); + auto typeface = getObject()->matchStyle(*fontStyle); + if (!typeface) { + return jsi::Value::null(); + } + return JsiSkTypeface::toValue(runtime, getContext(), std::move(typeface)); + } + + size_t getMemoryPressure() const override { return 2048; } + + std::string getObjectType() const override { return "JsiSkFontStyleSet"; } + + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkFontStyleSet, count), + JSI_EXPORT_FUNC(JsiSkFontStyleSet, getStyle), + JSI_EXPORT_FUNC(JsiSkFontStyleSet, createTypeface), + JSI_EXPORT_FUNC(JsiSkFontStyleSet, matchStyle), + JSI_EXPORT_FUNC(JsiSkFontStyleSet, dispose)) +}; + +} // namespace RNSkia diff --git a/packages/skia/src/skia/types/Font/FontMgr.ts b/packages/skia/src/skia/types/Font/FontMgr.ts index 7dce039b1a..7549f7864c 100644 --- a/packages/skia/src/skia/types/Font/FontMgr.ts +++ b/packages/skia/src/skia/types/Font/FontMgr.ts @@ -3,8 +3,35 @@ import type { SkTypeface } from "../Typeface"; import type { FontStyle } from "./Font"; +/** + * Style metadata for a single font variant: its weight, width, slant, and + * postscript name. Returned by {@link SkFontStyleSet.getStyle}. + */ +export interface FontStyleEntry extends Required { + name: string; +} + +/** + * An enumerable set of font variants for a single font family. + * Obtained via {@link SkFontMgr.matchFamily} or {@link SkFontMgr.createStyleSet}. + */ +export interface SkFontStyleSet extends SkJSIInstance<"FontStyleSet"> { + /** Number of variants in this set. */ + count(): number; + /** Style metadata for the variant at the given index (must be in [0, count)). */ + getStyle(index: number): FontStyleEntry; + /** Loads the exact typeface for a variant by index, or null if unavailable. */ + createTypeface(index: number): SkTypeface | null; + /** Returns the typeface whose style most closely matches the given style. */ + matchStyle(style: FontStyle): SkTypeface | null; +} + export interface SkFontMgr extends SkJSIInstance<"FontMgr"> { countFamilies(): number; getFamilyName(index: number): string; + /** Returns the style set for the family at the given index, or null. */ + createStyleSet(index: number): SkFontStyleSet | null; + /** Returns the style set for the named font family, or null if not found. */ + matchFamily(name: string): SkFontStyleSet | null; matchFamilyStyle(name: string, style: FontStyle): SkTypeface; } diff --git a/packages/skia/src/skia/web/JsiSkFontMgr.ts b/packages/skia/src/skia/web/JsiSkFontMgr.ts index 79a3448c6a..3632e64b5d 100644 --- a/packages/skia/src/skia/web/JsiSkFontMgr.ts +++ b/packages/skia/src/skia/web/JsiSkFontMgr.ts @@ -1,6 +1,6 @@ import type { CanvasKit, FontMgr } from "canvaskit-wasm"; -import type { FontStyle, SkFontMgr, SkTypeface } from "../types"; +import type { FontStyle, SkFontMgr, SkFontStyleSet, SkTypeface } from "../types"; import { HostObject, throwNotImplementedOnRNWeb } from "./Host"; @@ -21,6 +21,12 @@ export class JsiSkFontMgr getFamilyName(index: number) { return this.ref.getFamilyName(index); } + createStyleSet(_index: number): SkFontStyleSet | null { + return throwNotImplementedOnRNWeb(); + } + matchFamily(_name: string): SkFontStyleSet | null { + return throwNotImplementedOnRNWeb(); + } matchFamilyStyle(_familyName: string, _fontStyle: FontStyle) { return throwNotImplementedOnRNWeb(); } diff --git a/packages/skia/src/skia/web/JsiSkFontStyleSet.ts b/packages/skia/src/skia/web/JsiSkFontStyleSet.ts new file mode 100644 index 0000000000..77bedb9bcd --- /dev/null +++ b/packages/skia/src/skia/web/JsiSkFontStyleSet.ts @@ -0,0 +1,34 @@ +import type { CanvasKit } from "canvaskit-wasm"; + +import type { + FontStyle, + FontStyleEntry, + SkFontStyleSet, + SkTypeface, +} from "../types"; + +import { HostObject, throwNotImplementedOnRNWeb } from "./Host"; + +// SkFontStyleSet has no direct CanvasKit equivalent; this stub satisfies the +// interface on web while the native implementation uses JsiSkFontStyleSet.h. +export class JsiSkFontStyleSet + extends HostObject + implements SkFontStyleSet +{ + constructor(CanvasKit: CanvasKit) { + super(CanvasKit, {}, "FontStyleSet"); + } + + count() { + return throwNotImplementedOnRNWeb(); + } + getStyle(_index: number) { + return throwNotImplementedOnRNWeb(); + } + createTypeface(_index: number): SkTypeface | null { + return throwNotImplementedOnRNWeb(); + } + matchStyle(_style: FontStyle): SkTypeface | null { + return throwNotImplementedOnRNWeb(); + } +}