Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/workflows/build-npm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,37 @@ jobs:
with:
submodules: recursive

# The next channel ships the Graphite build; main ships the default (Ganesh)
# build. Both branches are identical — the graphite setup bakes the Dawn and
# Graphite headers into cpp/ and creates the libs/.graphite marker (shipped
# via the files field) that the podspec and build.gradle key off at build time.
- name: Setup
uses: ./.github/actions/setup
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
graphite: ${{ github.ref_name == 'next' }}

- name: Build package and documentation
run: yarn build

# Swap the prebuilt binary packages for their Graphite variants, pinned in
# graphiteDependencies (package.json). Only done at release time so that
# main and next stay identical.
- name: Swap in Graphite binary packages (next channel only)
if: github.ref_name == 'next'
working-directory: packages/skia
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
for (const name of Object.keys(pkg.dependencies)) {
if (name.startsWith('react-native-skia-')) delete pkg.dependencies[name];
}
Object.assign(pkg.dependencies, pkg.graphiteDependencies);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
node -p "JSON.stringify(require('./package.json').dependencies, null, 2)"

- name: Build NPM Package
working-directory: packages/skia
run: |
Expand Down
22 changes: 20 additions & 2 deletions apps/docs/docs/text/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,26 @@ The `fontStyle` object can have the following list of optional attributes:

- `fontFamily`: The name of the font family.
- `fontSize`: The size of the font.
- `fontStyle`: The slant of the font. Can be `normal`, `italic`, or `oblique`.
- `fontWeight`: The weight of the font. Can be `normal`, `bold`, or any of `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`.
- `fontStyle`: The slant of the font. Can be `normal`, `italic`, or `oblique`, or a `FontSlant` enum value.
- `fontWeight`: The weight of the font. Can be `normal`, `bold`, or any of `100`, `200`, `300`, `400`, `500`, `600`, `700`, `800`, `900`, or a `FontWeight` enum value.

The font style type is exported as `RNFontStyle` (with its `fontStyle` and `fontWeight` attributes typed as `RNFontSlant` and `RNFontWeight`), so you can type your own helpers.
`fontWeight` and `fontStyle` also accept the `FontWeight` and `FontSlant` enums used by the [Paragraph API](/docs/text/paragraph/), meaning the same style values can be shared between `matchFont` and a Paragraph `TextStyle` without any conversion:

```tsx twoslash
import {matchFont, FontWeight, FontSlant} from "@shopify/react-native-skia";
import type {RNFontStyle} from "@shopify/react-native-skia";

const labelStyle: Partial<RNFontStyle> = {
fontFamily: "Roboto",
fontSize: 16,
// FontWeight.Medium (500) and FontSlant.Italic are the same values
// you would use in a Paragraph TextStyle
fontWeight: FontWeight.Medium,
fontStyle: FontSlant.Italic,
};
const font = matchFont(labelStyle);
```

By default, `matchFont` uses the system font manager to match the font style. However, if you want to use your custom font manager, you can pass it as the second parameter to the `matchFont` function:

Expand Down
14 changes: 10 additions & 4 deletions packages/skia/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,18 @@ static def resolveNodePackage(packageName, baseDir) {
return proc.text.trim()
}

// Graphite is detected via a marker file created by install-skia-graphite, which
// downloads its binaries directly into libs/. For the default (Ganesh) build the
// binaries live in the react-native-skia-android npm package and are read in place.
// Graphite is detected via a marker file (created by install-skia-graphite for
// in-repo development, or shipped in the npm tarball for next-channel releases).
// The prebuilt binaries live in the react-native-skia-android npm package for the
// default (Ganesh) build and in react-native-skia-graphite-android for Graphite.
// In-repo Graphite development downloads them directly into libs/android instead,
// which takes precedence over the npm package when present.
def useGraphite = file("${projectDir}/../libs/.graphite").exists()
def localGraphiteLibs = file("${projectDir}/../libs/android")
def skiaLibsPath = useGraphite
? "${projectDir}/../libs/android"
? (localGraphiteLibs.exists()
? "${projectDir}/../libs/android"
: "${resolveNodePackage('react-native-skia-graphite-android', projectDir)}/libs")
: "${resolveNodePackage('react-native-skia-android', projectDir)}/libs"

logger.warn("react-native-skia: SK_GRAPHITE: ${useGraphite}")
Expand Down
32 changes: 29 additions & 3 deletions packages/skia/android/cpp/rnskia-android/OpenGLContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class OpenGLSharedContext {
_glConfig = _glDisplay->chooseConfig();
_glContext = _glDisplay->makeContext(_glConfig, nullptr);
_glSurface = _glDisplay->makePixelBufferSurface(_glConfig, 1, 1);
if (_glContext == nullptr || _glSurface == nullptr) {
RNSkLogger::logToConsole(
"Couldn't create the shared OpenGL context or surface");
return;
}
_glContext->makeCurrent(_glSurface.get());
}
};
Expand All @@ -60,6 +65,10 @@ class OpenGLContext {

sk_sp<SkSurface> MakeOffscreen(int width, int height,
bool useP3ColorSpace = false) {
if (_directContext == nullptr) {
return nullptr;
}

auto colorType = kRGBA_8888_SkColorType;

SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
Expand Down Expand Up @@ -110,6 +119,9 @@ class OpenGLContext {
sk_sp<SkImage> MakeImageFromBuffer(void *buffer,
bool requireKnownFormat = false) {
#if __ANDROID_API__ >= 26
if (_directContext == nullptr) {
return nullptr;
}
const AHardwareBuffer *hardwareBuffer =
static_cast<AHardwareBuffer *>(buffer);
DeleteImageProc deleteImageProc = nullptr;
Expand Down Expand Up @@ -181,6 +193,11 @@ class OpenGLContext {
// TODO: remove width, height
std::unique_ptr<WindowContext> MakeWindow(ANativeWindow *window,
bool highBitDepth = false) {
if (_directContext == nullptr) {
RNSkLogger::logToConsole(
"The OpenGL context is invalid, the surface will not be rendered");
return nullptr;
}
auto display = OpenGLSharedContext::getInstance().getDisplay();
if (highBitDepth) {
// A 10-bit window surface would require the shared EGL context to be
Expand All @@ -196,7 +213,11 @@ class OpenGLContext {
}

GrDirectContext *getDirectContext() { return _directContext.get(); }
void makeCurrent() { _glContext->makeCurrent(_glSurface.get()); }
void makeCurrent() {
if (_glContext != nullptr) {
_glContext->makeCurrent(_glSurface.get());
}
}

private:
std::unique_ptr<gl::Context> _glContext;
Expand All @@ -209,12 +230,17 @@ class OpenGLContext {
auto glConfig = OpenGLSharedContext::getInstance().getConfig();
_glContext = display->makeContext(glConfig, sharedContext);
_glSurface = display->makePixelBufferSurface(glConfig, 1, 1);
_glContext->makeCurrent(_glSurface.get());
if (_glContext == nullptr || _glSurface == nullptr ||
!_glContext->makeCurrent(_glSurface.get())) {
RNSkLogger::logToConsole(
"Couldn't create the OpenGL context, Skia rendering is disabled");
return;
}
auto backendInterface = GrGLMakeNativeInterface();
_directContext = GrDirectContexts::MakeGL(backendInterface);

if (_directContext == nullptr) {
throw std::runtime_error("GrDirectContexts::MakeGL failed");
RNSkLogger::logToConsole("GrDirectContexts::MakeGL failed");
}
}
};
Expand Down
11 changes: 9 additions & 2 deletions packages/skia/android/cpp/rnskia-android/OpenGLWindowContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ namespace RNSkia {

sk_sp<SkSurface> OpenGLWindowContext::getSurface() {
if (_skSurface == nullptr) {
_glContext->makeCurrent(_glSurface.get());
if (_glSurface == nullptr || !_glContext->makeCurrent(_glSurface.get())) {
RNSkLogger::logToConsole(
"Couldn't create the EGL window surface, the surface will not be "
"rendered");
return nullptr;
}
GLint stencil;
glGetIntegerv(GL_STENCIL_BITS, &stencil);

Expand Down Expand Up @@ -52,7 +57,9 @@ sk_sp<SkSurface> OpenGLWindowContext::getSurface() {
}

void OpenGLWindowContext::present() {
_glContext->makeCurrent(_glSurface.get());
if (_glSurface == nullptr || !_glContext->makeCurrent(_glSurface.get())) {
return;
}
_directContext->flushAndSubmit();
_glSurface->present();
}
Expand Down
3 changes: 3 additions & 0 deletions packages/skia/android/cpp/rnskia-android/gl/Context.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class Context {
if (_context == EGL_NO_CONTEXT) {
return false;
}
if (surface == nullptr || !surface->isValid()) {
return false;
}
const auto result =
eglMakeCurrentIfNecessary(_display, surface->getHandle(),
surface->getHandle(), _context) == EGL_TRUE;
Expand Down
2 changes: 1 addition & 1 deletion packages/skia/android/cpp/rnskia-android/gl/Surface.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Surface {
}
}

bool isValid() { return _surface != EGL_NO_SURFACE; }
bool isValid() const { return _surface != EGL_NO_SURFACE; }

const EGLSurface &getHandle() const { return _surface; }

Expand Down
4 changes: 4 additions & 0 deletions packages/skia/cpp/api/JsiSkiaContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class JsiSkiaContext : public JsiSkWrappingSharedPtrHostObject<WindowContext> {
}
auto result =
context->makeContextFromNativeSurface(surface, width, height);
if (result == nullptr) {
throw std::runtime_error(
"Couldn't create a Skia context from the native surface");
}
// Return the newly constructed object
auto hostObjectInstance =
std::make_shared<JsiSkiaContext>(context, std::move(result));
Expand Down
6 changes: 6 additions & 0 deletions packages/skia/cpp/rnskia/RNSkView.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class RNSkOffscreenCanvasProvider : public RNSkCanvasProvider {
Returns a snapshot of the current surface/canvas
*/
sk_sp<SkImage> makeSnapshot(SkRect *bounds) {
if (_surface == nullptr) {
return nullptr;
}
sk_sp<SkImage> image;
if (bounds != nullptr) {
SkIRect b =
Expand Down Expand Up @@ -120,6 +123,9 @@ class RNSkOffscreenCanvasProvider : public RNSkCanvasProvider {
Render to a canvas
*/
bool renderToCanvas(const std::function<void(SkCanvas *)> &cb) override {
if (_surface == nullptr) {
return false;
}
cb(_surface->getCanvas());
return true;
};
Expand Down
8 changes: 7 additions & 1 deletion packages/skia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"cpp/**/*.{h,cpp}",
"apple/**",
"react-native-skia.podspec",
"dist/**"
"dist/**",
"libs/.graphite"
],
"scripts": {
"lint": "eslint . --ext .ts,.tsx --max-warnings 0 --cache --fix",
Expand Down Expand Up @@ -140,6 +141,11 @@
"react-native-skia-apple-tvos": "150.0.0",
"react-reconciler": "0.31.0"
},
"graphiteDependencies": {
"react-native-skia-graphite-android": "150.0.0",
"react-native-skia-graphite-apple-ios": "150.0.0",
"react-native-skia-graphite-apple-macos": "150.0.0"
},
"eslintIgnore": [
"node_modules/",
"lib/"
Expand Down
28 changes: 18 additions & 10 deletions packages/skia/react-native-skia.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ end
# re-copied and CocoaPods picks up the change. This is best-effort: if `pod install`
# does not detect the change, a clean reinstall fixes it (acceptable until the upcoming
# Swift Package Manager migration).
install_apple_skia_libs = lambda do |base_dir|
{ 'ios' => 'react-native-skia-apple-ios',
'macos' => 'react-native-skia-apple-macos',
'tvos' => 'react-native-skia-apple-tvos' }.each do |platform, pkg_name|
install_apple_skia_libs = lambda do |base_dir, packages|
packages.each do |platform, pkg_name|
pkg_dir = resolve_node_package.call(pkg_name, base_dir)
next if pkg_dir.nil?

Expand All @@ -54,9 +52,18 @@ install_apple_skia_libs = lambda do |base_dir|
end
end

# Graphite downloads its binaries directly into libs/; only the default build needs
# the npm packages copied in.
install_apple_skia_libs.call(__dir__) unless use_graphite
# The default (Ganesh) build ships its binaries in the react-native-skia-apple-*
# npm packages, the Graphite build in react-native-skia-graphite-apple-* (no tvOS).
# During in-repo development install-skia-graphite downloads the binaries directly
# into libs/ and the graphite packages are absent from node_modules, in which case
# the copy below is a no-op and the downloaded binaries are used as-is.
apple_skia_packages = use_graphite ?
{ 'ios' => 'react-native-skia-graphite-apple-ios',
'macos' => 'react-native-skia-graphite-apple-macos' } :
{ 'ios' => 'react-native-skia-apple-ios',
'macos' => 'react-native-skia-apple-macos',
'tvos' => 'react-native-skia-apple-tvos' }
install_apple_skia_libs.call(__dir__, apple_skia_packages)

# Set preprocessor definitions based on GRAPHITE flag
preprocessor_defs = use_graphite ?
Expand All @@ -71,14 +78,15 @@ framework_names = ['libskia', 'libsvg', 'libskshaper', 'libskparagraph',
# Add Dawn library for Graphite builds (contains dawn::native symbols)
framework_names += ['libdawn_combined'] if use_graphite

# Verify that the prebuilt binaries are available (copied in above, or downloaded by
# install-skia-graphite for Graphite builds).
# Verify that the prebuilt binaries are available (copied in above from the npm
# packages, or downloaded by install-skia-graphite for in-repo Graphite builds).
unless Dir.exist?(File.join(__dir__, 'libs', 'ios')) && Dir.exist?(File.join(__dir__, 'libs', 'macos'))
expected_packages = apple_skia_packages.values.join(', ')
Pod::UI.warn "#{'-' * 72}"
Pod::UI.warn "react-native-skia: Skia prebuilt binaries not found in libs/!"
Pod::UI.warn ""
Pod::UI.warn "Make sure dependencies are installed (yarn install / npm install) so that"
Pod::UI.warn "the react-native-skia-apple-* packages are present, then run `pod install` again."
Pod::UI.warn "the #{expected_packages} packages are present, then run `pod install` again."
Pod::UI.warn "#{'-' * 72}"
raise "react-native-skia: Skia prebuilt binaries not found. Run `yarn install` then `pod install` to fix this."
end
Expand Down
13 changes: 6 additions & 7 deletions packages/skia/src/Platform/Platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@ import {
} from "react-native";

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";
import { isRNModule, unwrapModule } from "../skia/types";

import type { IPlatform } from "./IPlatform";

export const Platform: IPlatform = {
OS: RNPlatform.OS,
PixelRatio: PixelRatio.get(),
resolveAsset: (source: DataModule) => {
// eslint-disable-next-line no-nested-ternary
return isRNModule(source)
? Image.resolveAssetSource(source).uri
: "uri" in source
? source.uri
: source.default;
const asset = unwrapModule(source);
if (typeof asset === "string") {
return asset;
}
return isRNModule(asset) ? Image.resolveAssetSource(asset).uri : asset.uri;
},
findNodeHandle,
View,
Expand Down
17 changes: 9 additions & 8 deletions packages/skia/src/Platform/Platform.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, { useMemo } from "react";
import type { ViewComponent, ViewProps } from "react-native";

import type { DataModule } from "../skia/types";
import { isRNModule } from "../skia/types";
import { isRNModule, unwrapModule } from "../skia/types";

import type { IPlatform } from "./IPlatform";

Expand Down Expand Up @@ -40,23 +40,24 @@ export const Platform: IPlatform = {
OS: "web",
PixelRatio: typeof window !== "undefined" ? window.devicePixelRatio : 1, // window is not defined on node
resolveAsset: (source: DataModule) => {
if (isRNModule(source)) {
if (typeof source === "number" && typeof require === "function") {
const asset = unwrapModule(source);
if (typeof asset === "string") {
return asset;
}
if (isRNModule(asset)) {
if (typeof require === "function") {
const {
getAssetByID,
} = require("react-native/Libraries/Image/AssetRegistry");
const { httpServerLocation, name, type } = getAssetByID(source);
const { httpServerLocation, name, type } = getAssetByID(asset);
const uri = `${httpServerLocation}/${name}.${type}`;
return uri;
}
throw new Error(
"Asset source is a number - this is not supported on the web"
);
}
if ("uri" in source) {
return source.uri;
}
return source.default;
return asset.uri;
},
findNodeHandle: () => {
throw new Error("findNodeHandle is not supported on the web");
Expand Down
Loading
Loading