Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions apps/docs/docs/text/font-style-set.md
Original file line number Diff line number Diff line change
@@ -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<FontStyle> {
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.
24 changes: 23 additions & 1 deletion packages/skia/cpp/api/JsiSkFontMgr.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <vector>

#include "JsiSkFontStyle.h"
#include "JsiSkFontStyleSet.h"
#include "JsiSkHostObjects.h"
#include "RNSkLog.h"
#include <jsi/jsi.h>
Expand Down Expand Up @@ -69,13 +70,34 @@ class JsiSkFontMgr : public JsiSkWrappingSkPtrHostObject<SkFontMgr> {
runtime, hostObjectInstance, getContext());
}

JSI_HOST_FUNCTION(createStyleSet) {
auto index = static_cast<int>(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<std::string> _systemFontFamilies;
Expand Down
94 changes: 94 additions & 0 deletions packages/skia/cpp/api/JsiSkFontStyleSet.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#pragma once

#include <memory>
#include <utility>

#include <jsi/jsi.h>

#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<SkFontStyleSet> {
public:
EXPORT_JSI_API_TYPENAME(JsiSkFontStyleSet, FontStyleSet)

JsiSkFontStyleSet(std::shared_ptr<RNSkPlatformContext> context,
sk_sp<SkFontStyleSet> fontStyleSet)
: JsiSkWrappingSkPtrHostObject(std::move(context),
std::move(fontStyleSet)) {}

static jsi::Value toValue(jsi::Runtime &runtime,
std::shared_ptr<RNSkPlatformContext> context,
sk_sp<SkFontStyleSet> styleSet) {
auto hostObjectInstance =
std::make_shared<JsiSkFontStyleSet>(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<int>(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<int>(style.slant())));
result.setProperty(runtime, "name",
jsi::String::createFromUtf8(runtime, name.c_str()));
return result;
}

JSI_HOST_FUNCTION(createTypeface) {
auto index = static_cast<int>(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
27 changes: 27 additions & 0 deletions packages/skia/src/skia/types/Font/FontMgr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FontStyle> {
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;
}
8 changes: 7 additions & 1 deletion packages/skia/src/skia/web/JsiSkFontMgr.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -21,6 +21,12 @@ export class JsiSkFontMgr
getFamilyName(index: number) {
return this.ref.getFamilyName(index);
}
createStyleSet(_index: number): SkFontStyleSet | null {
return throwNotImplementedOnRNWeb<SkFontStyleSet | null>();
}
matchFamily(_name: string): SkFontStyleSet | null {
return throwNotImplementedOnRNWeb<SkFontStyleSet | null>();
}
matchFamilyStyle(_familyName: string, _fontStyle: FontStyle) {
return throwNotImplementedOnRNWeb<SkTypeface>();
}
Expand Down
34 changes: 34 additions & 0 deletions packages/skia/src/skia/web/JsiSkFontStyleSet.ts
Original file line number Diff line number Diff line change
@@ -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<object, "FontStyleSet">
implements SkFontStyleSet
{
constructor(CanvasKit: CanvasKit) {
super(CanvasKit, {}, "FontStyleSet");
}

count() {
return throwNotImplementedOnRNWeb<number>();
}
getStyle(_index: number) {
return throwNotImplementedOnRNWeb<FontStyleEntry>();
}
createTypeface(_index: number): SkTypeface | null {
return throwNotImplementedOnRNWeb<SkTypeface | null>();
}
matchStyle(_style: FontStyle): SkTypeface | null {
return throwNotImplementedOnRNWeb<SkTypeface | null>();
}
}
Loading