react-native-vroom-chart 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/CMakeLists.txt +143 -0
- package/android/README.md +60 -5
- package/android/build.gradle +115 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/VroomChartJni.cpp +24 -0
- package/android/src/main/java/com/vroom/chart/VroomChartModule.kt +31 -0
- package/android/src/main/java/com/vroom/chart/VroomChartPackage.kt +31 -0
- package/cpp/VroomChartHostObject.cpp +218 -1
- package/cpp/VroomJsiInstaller.cpp +11 -1
- package/cpp/VroomSkiaContext.cpp +15 -7
- package/cpp/_core_include/vroom/vroom_chart.h +70 -0
- package/cpp/_core_src/chart.cpp +7 -0
- package/cpp/_core_src/chart.h +27 -0
- package/cpp/_core_src/chart_facade.cpp +91 -0
- package/cpp/_core_src/price_line_layout.cpp +87 -0
- package/cpp/_core_src/price_line_layout.h +80 -0
- package/cpp/_core_src/price_lines.cpp +428 -0
- package/cpp/_core_src/price_lines.h +56 -0
- package/lib/index.d.mts +104 -1
- package/lib/index.d.ts +104 -1
- package/lib/index.js +98 -7
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +98 -7
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/VroomChart.tsx +93 -8
- package/src/index.ts +2 -0
- package/src/jsi.d.ts +48 -0
- package/src/types.ts +2 -0
- package/src/useChartCore.ts +65 -3
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# android/CMakeLists.txt
|
|
2
|
+
#
|
|
3
|
+
# Builds libvroomchart.so from the same platform-agnostic JSI glue and chart
|
|
4
|
+
# core the iOS podspec compiles (see ../react-native-vroom-chart.podspec),
|
|
5
|
+
# plus this directory's small JNI entry point (src/main/cpp/VroomChartJni.cpp).
|
|
6
|
+
#
|
|
7
|
+
# RN-Skia's headers (JsiSkHostObjects.h, RNSkPlatformContext.h — needed by
|
|
8
|
+
# ../cpp/VroomSkiaContext.cpp) aren't consumed via CMake's prefab mechanism;
|
|
9
|
+
# instead we point directly at @shopify/react-native-skia's `cpp/` sources,
|
|
10
|
+
# mirroring the podspec's `require.resolve('@shopify/react-native-skia/package.json')`
|
|
11
|
+
# trick (VROOM_SKIA_CPP_DIR is computed the same way in build.gradle).
|
|
12
|
+
#
|
|
13
|
+
# IMPORTANT — SK_GRAPHITE ABI note: RNSkPlatformContext's vtable layout
|
|
14
|
+
# differs depending on whether SK_GRAPHITE was defined when RN-Skia's own
|
|
15
|
+
# Android build compiled RNSkAndroidPlatformContext (the concrete class whose
|
|
16
|
+
# vtable we call into via VroomSkiaContext.cpp's `ctx->createFontMgr()`).
|
|
17
|
+
# We must define SK_GRAPHITE identically to however RN-Skia's own CMakeLists
|
|
18
|
+
# (android/CMakeLists.txt) decided it, or virtual calls resolve to the wrong
|
|
19
|
+
# vtable slot. We replicate their libskia.a symbol probe below rather than
|
|
20
|
+
# hard-coding an assumption.
|
|
21
|
+
cmake_minimum_required(VERSION 3.22)
|
|
22
|
+
project(vroomchart)
|
|
23
|
+
|
|
24
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
25
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
26
|
+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
|
27
|
+
|
|
28
|
+
find_package(ReactAndroid REQUIRED CONFIG)
|
|
29
|
+
# Prefab package name matches the Gradle project name AGP autolinking assigns
|
|
30
|
+
# RN-Skia (":shopify_react-native-skia"), not the "rnskia" module name inside
|
|
31
|
+
# its own `prefab { rnskia { ... } }` block — that block key becomes the
|
|
32
|
+
# `shopify_react-native-skia::rnskia` *target* name below, not the package.
|
|
33
|
+
find_package(shopify_react-native-skia REQUIRED CONFIG)
|
|
34
|
+
|
|
35
|
+
if(NOT DEFINED VROOM_SKIA_CPP_DIR)
|
|
36
|
+
message(FATAL_ERROR "VROOM_SKIA_CPP_DIR not set — passed from android/build.gradle.")
|
|
37
|
+
endif()
|
|
38
|
+
|
|
39
|
+
# RN-Skia's own package root is one level up from its cpp/ dir; its prebuilt
|
|
40
|
+
# Skia static libs live at <root>/libs/android/<abi>/libskia.a.
|
|
41
|
+
get_filename_component(VROOM_SKIA_PKG_DIR "${VROOM_SKIA_CPP_DIR}" DIRECTORY)
|
|
42
|
+
set(VROOM_SKIA_LIBS_DIR "${VROOM_SKIA_PKG_DIR}/libs/android/${ANDROID_ABI}")
|
|
43
|
+
|
|
44
|
+
# Our chart core calls Skia types (SkCanvas, SkPaint, SkFont, ...) and
|
|
45
|
+
# RN-Skia's own JSI plumbing (RNJsi::JsiHostObject, JsiSkPicture, ...)
|
|
46
|
+
# directly — none of that is header-only, and we need *both* libraries:
|
|
47
|
+
# - librnskia.so (RN-Skia's prefab-published `rnskia` module, consumed above
|
|
48
|
+
# via `implementation project(":shopify_react-native-skia")`) for RNJsi's
|
|
49
|
+
# own symbols (JsiHostObject, JsiSkPicture, ...), which only exist there.
|
|
50
|
+
# - libskia.a (RN-Skia's bundled prebuilt Skia static lib) for direct Skia
|
|
51
|
+
# calls (SkCanvas::drawRect, SkFont, ...): librnskia.so links libskia.a
|
|
52
|
+
# internally but doesn't re-export its symbols, so linking librnskia.so
|
|
53
|
+
# alone leaves every direct Skia call undefined.
|
|
54
|
+
# Recompiling RN-Skia's cpp/jsi/*.cpp ourselves instead of linking librnskia.so
|
|
55
|
+
# was considered and rejected — it would duplicate RNJsi's symbols (and their
|
|
56
|
+
# static/global state) across two separately-loaded .so's, which is fine on
|
|
57
|
+
# iOS's single static binary but not here.
|
|
58
|
+
add_library(vroom_skia_prebuilt STATIC IMPORTED)
|
|
59
|
+
set_property(TARGET vroom_skia_prebuilt PROPERTY
|
|
60
|
+
IMPORTED_LOCATION "${VROOM_SKIA_LIBS_DIR}/libskia.a")
|
|
61
|
+
|
|
62
|
+
set(SK_GRAPHITE_AVAILABLE OFF)
|
|
63
|
+
if(EXISTS "${VROOM_SKIA_LIBS_DIR}/libskia.a")
|
|
64
|
+
execute_process(
|
|
65
|
+
COMMAND nm "${VROOM_SKIA_LIBS_DIR}/libskia.a"
|
|
66
|
+
COMMAND grep "dawn::\\|wgpu\\|_ZN4dawn\\|DawnDevice\\|dawn_native"
|
|
67
|
+
OUTPUT_VARIABLE VROOM_NM_OUTPUT
|
|
68
|
+
ERROR_QUIET
|
|
69
|
+
RESULT_VARIABLE VROOM_NM_RESULT
|
|
70
|
+
)
|
|
71
|
+
if(VROOM_NM_RESULT EQUAL 0 AND NOT "${VROOM_NM_OUTPUT}" STREQUAL "")
|
|
72
|
+
set(SK_GRAPHITE_AVAILABLE ON)
|
|
73
|
+
endif()
|
|
74
|
+
endif()
|
|
75
|
+
|
|
76
|
+
# The chart core (shared across iOS/Android/WASM — see ../../core/CMakeLists.txt).
|
|
77
|
+
set(VROOM_CORE_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/_core_include")
|
|
78
|
+
set(VROOM_CORE_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cpp/_core_src")
|
|
79
|
+
file(GLOB VROOM_CORE_SOURCES CONFIGURE_DEPENDS "${VROOM_CORE_SRC_DIR}/*.cpp")
|
|
80
|
+
|
|
81
|
+
add_library(vroomchart SHARED
|
|
82
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../cpp/VroomJsiInstaller.cpp"
|
|
83
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../cpp/VroomChartHostObject.cpp"
|
|
84
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../cpp/VroomSkiaContext.cpp"
|
|
85
|
+
${VROOM_CORE_SOURCES}
|
|
86
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/VroomChartJni.cpp"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
target_include_directories(vroomchart PRIVATE
|
|
90
|
+
"${VROOM_CORE_INCLUDE_DIR}"
|
|
91
|
+
"${VROOM_CORE_SRC_DIR}"
|
|
92
|
+
"${CMAKE_CURRENT_SOURCE_DIR}/../cpp"
|
|
93
|
+
# RN-Skia headers — mirrors its own android/CMakeLists.txt include list.
|
|
94
|
+
"${VROOM_SKIA_CPP_DIR}"
|
|
95
|
+
"${VROOM_SKIA_CPP_DIR}/api"
|
|
96
|
+
"${VROOM_SKIA_CPP_DIR}/jsi"
|
|
97
|
+
"${VROOM_SKIA_CPP_DIR}/rnskia"
|
|
98
|
+
"${VROOM_SKIA_CPP_DIR}/rnskia/values"
|
|
99
|
+
"${VROOM_SKIA_CPP_DIR}/utils"
|
|
100
|
+
"${VROOM_SKIA_CPP_DIR}/skia"
|
|
101
|
+
"${VROOM_SKIA_CPP_DIR}/dawn/include"
|
|
102
|
+
# RNSkPlatformContext.h pulls in ReactCommon/CallInvoker.h directly, which
|
|
103
|
+
# isn't part of the prefab-published jsi headers — same dirs RN-Skia's own
|
|
104
|
+
# android/CMakeLists.txt adds for the same reason.
|
|
105
|
+
"${REACT_NATIVE_DIR}/ReactCommon"
|
|
106
|
+
"${REACT_NATIVE_DIR}/ReactCommon/callinvoker"
|
|
107
|
+
"${REACT_NATIVE_DIR}/ReactCommon/runtimeexecutor"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
target_compile_definitions(vroomchart PRIVATE
|
|
111
|
+
SK_BUILD_FOR_ANDROID
|
|
112
|
+
SK_DISABLE_LEGACY_SHAPER_FACTORY
|
|
113
|
+
SK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API
|
|
114
|
+
ONANDROID
|
|
115
|
+
ON_ANDROID
|
|
116
|
+
)
|
|
117
|
+
if(SK_GRAPHITE_AVAILABLE)
|
|
118
|
+
target_compile_definitions(vroomchart PRIVATE SK_GRAPHITE)
|
|
119
|
+
else()
|
|
120
|
+
target_compile_definitions(vroomchart PRIVATE SK_GL SK_GANESH)
|
|
121
|
+
endif()
|
|
122
|
+
|
|
123
|
+
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
|
|
124
|
+
include("${REACT_NATIVE_DIR}/ReactCommon/cmake-utils/react-native-flags.cmake")
|
|
125
|
+
target_compile_reactnative_options(vroomchart PRIVATE)
|
|
126
|
+
else()
|
|
127
|
+
target_compile_options(vroomchart PRIVATE -fexceptions -frtti -std=c++20 -Wall)
|
|
128
|
+
endif()
|
|
129
|
+
|
|
130
|
+
# ../cpp/_core_include/vroom/vroom_chart.h forward-declares `struct SkCanvas;`
|
|
131
|
+
# (to keep the public C facade header Skia-free); Skia itself declares
|
|
132
|
+
# `class SkCanvas`. This tag mismatch is harmless but react-native-flags.cmake
|
|
133
|
+
# enables -Werror, which would otherwise turn it into a build failure.
|
|
134
|
+
target_compile_options(vroomchart PRIVATE -Wno-mismatched-tags)
|
|
135
|
+
|
|
136
|
+
find_library(VROOM_LOG_LIB log)
|
|
137
|
+
|
|
138
|
+
target_link_libraries(vroomchart
|
|
139
|
+
${VROOM_LOG_LIB}
|
|
140
|
+
ReactAndroid::jsi
|
|
141
|
+
shopify_react-native-skia::rnskia
|
|
142
|
+
vroom_skia_prebuilt
|
|
143
|
+
)
|
package/android/README.md
CHANGED
|
@@ -1,7 +1,62 @@
|
|
|
1
|
-
# Android
|
|
1
|
+
# Android bridge
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Android equivalent of `../ios/`: a small TurboModule
|
|
4
|
+
([`VroomChartModule.kt`](src/main/java/com/vroom/chart/VroomChartModule.kt)) whose
|
|
5
|
+
`install()` hands the JSI runtime pointer to a native `vroomchart` library
|
|
6
|
+
(built by [`CMakeLists.txt`](CMakeLists.txt)) that installs
|
|
7
|
+
`global.VroomChartJSI` via the same platform-agnostic
|
|
8
|
+
[`../cpp/VroomJsiInstaller.cpp`](../cpp/VroomJsiInstaller.cpp) the iOS bridge
|
|
9
|
+
uses.
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
The chart core itself (`../cpp/_core_src`) and the RN-Skia font/context glue
|
|
12
|
+
(`../cpp/VroomSkiaContext.*`) are unmodified and shared across iOS and
|
|
13
|
+
Android — only the platform glue (this directory + `../ios/`) differs.
|
|
14
|
+
`../cpp/VroomChartHostObject.cpp` has one small `#if defined(__ANDROID__)`
|
|
15
|
+
branch (see "Cross-`.so` Skia objects" below).
|
|
16
|
+
|
|
17
|
+
`build.gradle` resolves `@shopify/react-native-skia`'s `cpp/` sources via
|
|
18
|
+
Node module resolution (mirroring `../react-native-vroom-chart.podspec`'s
|
|
19
|
+
`require.resolve` trick) so `VroomSkiaContext.cpp` can include RN-Skia's
|
|
20
|
+
`JsiSkHostObjects.h` / `RNSkPlatformContext.h`. `CMakeLists.txt` also
|
|
21
|
+
replicates RN-Skia's own Graphite-vs-Ganesh detection (probing its bundled
|
|
22
|
+
`libskia.a`) so `SK_GRAPHITE` is defined identically to however RN-Skia's own
|
|
23
|
+
Android build was compiled — `RNSkPlatformContext`'s vtable layout depends on
|
|
24
|
+
that macro, and a mismatch would corrupt the virtual call in
|
|
25
|
+
`VroomJsiInstaller.cpp`'s `ensureAxisTypeface`.
|
|
26
|
+
|
|
27
|
+
We link two RN-Skia-adjacent libraries into `libvroomchart.so`, for two
|
|
28
|
+
different reasons:
|
|
29
|
+
- `shopify_react-native-skia::rnskia` (RN-Skia's own prefab-published
|
|
30
|
+
shared library, `librnskia.so`) — for `RNJsi::JsiHostObject` and friends,
|
|
31
|
+
whose out-of-line methods are compiled only there.
|
|
32
|
+
- RN-Skia's bundled `libskia.a` (imported directly by path, since it isn't
|
|
33
|
+
prefab-published) — for direct Skia calls (`SkCanvas::drawRect`, `SkFont`,
|
|
34
|
+
...), since `librnskia.so` links `libskia.a` internally but doesn't
|
|
35
|
+
re-export its symbols.
|
|
36
|
+
|
|
37
|
+
### Cross-`.so` Skia objects
|
|
38
|
+
|
|
39
|
+
Unlike iOS (one static binary), Android compiles RN-Skia into its own
|
|
40
|
+
`librnskia.so`, separate from `libvroomchart.so`. That's transparent for most
|
|
41
|
+
of the JSI bridge, but **`RNSkia::JsiSkPicture` — the object `render()` /
|
|
42
|
+
`pan()` / etc. return to JS — can't be constructed directly in
|
|
43
|
+
`libvroomchart.so` on Android.** RN-Skia's own C++ (e.g.
|
|
44
|
+
`cpp/api/recorder/Convertor.h`'s `getPropertyValue<sk_sp<SkPicture>>`, which
|
|
45
|
+
runs whenever `<Picture>` reads the value) does
|
|
46
|
+
`value.asObject(rt).asHostObject<JsiSkPicture>(rt)`, a `dynamic_pointer_cast`
|
|
47
|
+
under the hood. A `JsiSkPicture` we construct ourselves has a vtable/typeinfo
|
|
48
|
+
compiled into *our* `.so`; that cast — running inside `librnskia.so` — sees a
|
|
49
|
+
different, unmerged RTTI record for "the same" class and fails with `Object
|
|
50
|
+
is not a HostObject of desired type`.
|
|
51
|
+
|
|
52
|
+
`VroomChartHostObject.cpp`'s `wrapPicture()` works around this only on
|
|
53
|
+
Android: instead of constructing `RNSkia::JsiSkPicture` directly (as iOS
|
|
54
|
+
does), it serializes the `SkPicture` (`SkPicture::serialize()`) and calls
|
|
55
|
+
RN-Skia's own public JS API, `Skia.Picture.MakePicture(bytes)`, via JSI. That
|
|
56
|
+
runs `JsiSkPictureFactory::MakePicture` — genuinely compiled inside
|
|
57
|
+
`librnskia.so` — so the returned object's RTTI matches what
|
|
58
|
+
`librnskia.so`'s own code expects. This costs a serialize + re-parse of the
|
|
59
|
+
picture's draw commands on every `render()`/gesture call, which is real
|
|
60
|
+
overhead on what's meant to be a 60fps hot path; if that shows up in
|
|
61
|
+
profiling, revisit (e.g. a merged-`.so` build, or a lighter-weight bridge
|
|
62
|
+
that avoids the round trip).
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// android/build.gradle
|
|
2
|
+
//
|
|
3
|
+
// Builds the Android half of the JSI bridge: a small `vroomchart` native
|
|
4
|
+
// library (see CMakeLists.txt) plus the TurboModule that installs it
|
|
5
|
+
// (see src/main/java/com/vroom/chart). Modeled on
|
|
6
|
+
// @shopify/react-native-skia/android/build.gradle and
|
|
7
|
+
// react-native-worklets/android/build.gradle, which solve the same
|
|
8
|
+
// "compile custom C++ against the RN/RN-Skia JSI internals" problem.
|
|
9
|
+
|
|
10
|
+
def safeExtGet(prop, fallback) {
|
|
11
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
def reactNativeArchitectures() {
|
|
15
|
+
def value = project.getProperties().get("reactNativeArchitectures")
|
|
16
|
+
return value ? value.split(",") : ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Resolves a package's directory via Node's own module resolution, so this
|
|
20
|
+
// works whether react-native-vroom-chart is installed flat or (as in this
|
|
21
|
+
// monorepo) via pnpm's nested .pnpm store.
|
|
22
|
+
def resolvePackageDir(packageJsonSpecifier) {
|
|
23
|
+
def packageJsonPath = providers.exec {
|
|
24
|
+
workingDir(rootDir)
|
|
25
|
+
commandLine("node", "--print", "require.resolve('${packageJsonSpecifier}')")
|
|
26
|
+
}.standardOutput.asText.get().trim()
|
|
27
|
+
return file(packageJsonPath).parentFile
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def reactNativeDir = resolvePackageDir("react-native/package.json")
|
|
31
|
+
def skiaCppDir = new File(resolvePackageDir("@shopify/react-native-skia/package.json"), "cpp")
|
|
32
|
+
|
|
33
|
+
// Mirror ../cpp/_core_{src,include} from ../../core/{src,include} whenever
|
|
34
|
+
// the monorepo core is present, so edits to packages/core/ propagate on the
|
|
35
|
+
// next build without a separate vendoring step. This matches
|
|
36
|
+
// ../react-native-vroom-chart.podspec's iOS behavior; a published package
|
|
37
|
+
// (no ../../core) falls back to the committed vendored copies instead
|
|
38
|
+
// (see ../scripts/vendor-core.mjs).
|
|
39
|
+
def coreRoot = file("${projectDir}/../../core")
|
|
40
|
+
if (coreRoot.isDirectory()) {
|
|
41
|
+
["src", "include"].each { sub ->
|
|
42
|
+
def dst = file("${projectDir}/../cpp/_core_${sub}")
|
|
43
|
+
delete dst
|
|
44
|
+
copy {
|
|
45
|
+
from "${coreRoot}/${sub}"
|
|
46
|
+
into dst
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
apply plugin: "com.android.library"
|
|
52
|
+
apply plugin: "kotlin-android"
|
|
53
|
+
apply plugin: "com.facebook.react"
|
|
54
|
+
|
|
55
|
+
android {
|
|
56
|
+
namespace "com.vroom.chart"
|
|
57
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
58
|
+
|
|
59
|
+
if (rootProject.hasProperty("ndkPath")) {
|
|
60
|
+
ndkPath rootProject.ext.ndkPath
|
|
61
|
+
}
|
|
62
|
+
if (rootProject.hasProperty("ndkVersion")) {
|
|
63
|
+
ndkVersion rootProject.ext.ndkVersion
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
defaultConfig {
|
|
67
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
68
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
69
|
+
|
|
70
|
+
externalNativeBuild {
|
|
71
|
+
cmake {
|
|
72
|
+
abiFilters(*reactNativeArchitectures())
|
|
73
|
+
arguments "-DANDROID_STL=c++_shared",
|
|
74
|
+
"-DREACT_NATIVE_DIR=${reactNativeDir}",
|
|
75
|
+
"-DVROOM_SKIA_CPP_DIR=${skiaCppDir}"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
buildFeatures {
|
|
81
|
+
// Consumes the `ReactAndroid` prefab package (published by
|
|
82
|
+
// com.facebook.react:react-android) so CMake can `find_package(ReactAndroid)`
|
|
83
|
+
// for the jsi headers/library without us hand-resolving RN's AAR layout.
|
|
84
|
+
prefab true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
externalNativeBuild {
|
|
88
|
+
cmake {
|
|
89
|
+
path "CMakeLists.txt"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lintOptions {
|
|
94
|
+
abortOnError false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
repositories {
|
|
99
|
+
maven {
|
|
100
|
+
url("${reactNativeDir}/android")
|
|
101
|
+
}
|
|
102
|
+
google()
|
|
103
|
+
mavenCentral()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
dependencies {
|
|
107
|
+
implementation "com.facebook.react:react-android"
|
|
108
|
+
// ../cpp/VroomChartHostObject.cpp constructs RN-Skia JSI host objects
|
|
109
|
+
// (JsiSkPicture, etc.) whose base class (RNJsi::JsiHostObject) has
|
|
110
|
+
// out-of-line methods compiled into RN-Skia's own native library — we link
|
|
111
|
+
// against its prefab-published `rnskia` package (below, in CMakeLists.txt)
|
|
112
|
+
// rather than recompiling those .cpp files ourselves, to avoid duplicate
|
|
113
|
+
// definitions of RNSkia's internals across two separately-loaded .so's.
|
|
114
|
+
implementation project(":shopify_react-native-skia")
|
|
115
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// JNI entry point for VroomChartModule.install() (see
|
|
2
|
+
// ../java/com/vroom/chart/VroomChartModule.kt). This is the Android
|
|
3
|
+
// equivalent of ../../ios/VroomChartModule.mm's `-install` method: it
|
|
4
|
+
// reinterprets the JSI runtime pointer handed over from Java and calls the
|
|
5
|
+
// same platform-agnostic vroom::installJsi() the iOS bridge uses.
|
|
6
|
+
|
|
7
|
+
#include <jni.h>
|
|
8
|
+
|
|
9
|
+
#include <jsi/jsi.h>
|
|
10
|
+
|
|
11
|
+
#include "VroomJsiInstaller.h"
|
|
12
|
+
|
|
13
|
+
extern "C" JNIEXPORT jboolean JNICALL
|
|
14
|
+
Java_com_vroom_chart_VroomChartModule_nativeInstall(JNIEnv* /*env*/,
|
|
15
|
+
jobject /*thiz*/,
|
|
16
|
+
jlong runtimePointer) {
|
|
17
|
+
auto* runtime =
|
|
18
|
+
reinterpret_cast<facebook::jsi::Runtime*>(runtimePointer);
|
|
19
|
+
if (runtime == nullptr) {
|
|
20
|
+
return JNI_FALSE;
|
|
21
|
+
}
|
|
22
|
+
vroom::installJsi(*runtime);
|
|
23
|
+
return JNI_TRUE;
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.vroom.chart
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
|
|
6
|
+
// TurboModule counterpart to ../../ios/VroomChartModule.mm. `install()` hands
|
|
7
|
+
// the JSI runtime pointer to the native `vroomchart` library (see
|
|
8
|
+
// ../../CMakeLists.txt), which installs `global.VroomChartJSI` via the same
|
|
9
|
+
// platform-agnostic vroom::installJsi() the iOS bridge calls.
|
|
10
|
+
//
|
|
11
|
+
// `NativeVroomChartSpec` is generated by React Native's codegen from
|
|
12
|
+
// ../../../src/NativeVroomChart.ts (codegenConfig.android.javaPackageName in
|
|
13
|
+
// package.json) — the same class name iOS's codegen generates.
|
|
14
|
+
@ReactModule(name = NativeVroomChartSpec.NAME)
|
|
15
|
+
class VroomChartModule(reactContext: ReactApplicationContext) :
|
|
16
|
+
NativeVroomChartSpec(reactContext) {
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
init {
|
|
20
|
+
System.loadLibrary("vroomchart")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private external fun nativeInstall(runtimePointer: Long): Boolean
|
|
25
|
+
|
|
26
|
+
override fun install(): Boolean {
|
|
27
|
+
val runtimePointer =
|
|
28
|
+
reactApplicationContext.javaScriptContextHolder?.get() ?: return false
|
|
29
|
+
return nativeInstall(runtimePointer)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.vroom.chart
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class VroomChartPackage : BaseReactPackage() {
|
|
10
|
+
|
|
11
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
12
|
+
if (name == NativeVroomChartSpec.NAME) {
|
|
13
|
+
VroomChartModule(reactContext)
|
|
14
|
+
} else {
|
|
15
|
+
null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
19
|
+
mutableMapOf(
|
|
20
|
+
NativeVroomChartSpec.NAME to
|
|
21
|
+
ReactModuleInfo(
|
|
22
|
+
NativeVroomChartSpec.NAME,
|
|
23
|
+
VroomChartModule::class.java.name,
|
|
24
|
+
/* canOverrideExistingModule = */ false,
|
|
25
|
+
/* needsEagerInit = */ false,
|
|
26
|
+
/* isCxxModule = */ false,
|
|
27
|
+
/* isTurboModule = */ true,
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#include "VroomChartHostObject.h"
|
|
2
2
|
|
|
3
3
|
#include <cstring>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
4
6
|
|
|
5
7
|
#include "chart_internal.h"
|
|
6
8
|
#include "vroom/vroom_chart.h"
|
|
@@ -27,7 +29,7 @@ ChartHostObject::~ChartHostObject() {
|
|
|
27
29
|
std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
28
30
|
jsi::Runtime& rt) {
|
|
29
31
|
std::vector<jsi::PropNameID> out;
|
|
30
|
-
out.reserve(
|
|
32
|
+
out.reserve(29);
|
|
31
33
|
out.push_back(jsi::PropNameID::forAscii(rt, "setCandles"));
|
|
32
34
|
out.push_back(jsi::PropNameID::forAscii(rt, "setSize"));
|
|
33
35
|
out.push_back(jsi::PropNameID::forAscii(rt, "setColor"));
|
|
@@ -52,10 +54,76 @@ std::vector<jsi::PropNameID> ChartHostObject::getPropertyNames(
|
|
|
52
54
|
out.push_back(jsi::PropNameID::forAscii(rt, "setOverlays"));
|
|
53
55
|
out.push_back(jsi::PropNameID::forAscii(rt, "setVWAP"));
|
|
54
56
|
out.push_back(jsi::PropNameID::forAscii(rt, "setBollinger"));
|
|
57
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "coordAt"));
|
|
58
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLines"));
|
|
59
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "hitTestPriceLine"));
|
|
60
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineHover"));
|
|
61
|
+
out.push_back(jsi::PropNameID::forAscii(rt, "setPriceLineDrag"));
|
|
55
62
|
out.push_back(jsi::PropNameID::forAscii(rt, "render"));
|
|
56
63
|
return out;
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
#if defined(__ANDROID__)
|
|
67
|
+
// Android only: RN-Skia is compiled into its own librnskia.so, separate from
|
|
68
|
+
// libvroomchart.so. Constructing a RNSkia::JsiSkPicture directly here (as iOS
|
|
69
|
+
// does, below) gives it a vtable/typeinfo from *our* .so; when RN-Skia's own
|
|
70
|
+
// compiled code later consumes it (e.g. Convertor.h's
|
|
71
|
+
// `getPropertyValue<sk_sp<SkPicture>>`, which does
|
|
72
|
+
// `value.asObject(rt).asHostObject<JsiSkPicture>(rt)` — a dynamic_pointer_cast
|
|
73
|
+
// under the hood), the cast fails cross-.so with "Object is not a HostObject
|
|
74
|
+
// of desired type", because the object's *runtime* type was never actually
|
|
75
|
+
// compiled inside librnskia.so. iOS statically links everything into one
|
|
76
|
+
// binary, so no such mismatch exists there.
|
|
77
|
+
//
|
|
78
|
+
// The fix: build the picture through RN-Skia's own public JS API instead
|
|
79
|
+
// (`Skia.Picture.MakePicture(bytes)`, the same call `require(...).png`-style
|
|
80
|
+
// static pictures use), so the resulting JsiSkPicture is genuinely
|
|
81
|
+
// constructed by librnskia.so's own code. This costs a serialize +
|
|
82
|
+
// re-parse of the picture's draw ops per call — real overhead on a gesture
|
|
83
|
+
// hot path — but is the only ABI-safe option found so far. Revisit if
|
|
84
|
+
// profiling shows this mattering (e.g. a merged-.so build, or a lighter
|
|
85
|
+
// bridge that avoids the round trip).
|
|
86
|
+
#include "include/core/SkData.h"
|
|
87
|
+
|
|
88
|
+
namespace {
|
|
89
|
+
class SkDataMutableBuffer : public facebook::jsi::MutableBuffer {
|
|
90
|
+
public:
|
|
91
|
+
explicit SkDataMutableBuffer(sk_sp<SkData> data) : data_(std::move(data)) {}
|
|
92
|
+
size_t size() const override { return data_->size(); }
|
|
93
|
+
uint8_t* data() override {
|
|
94
|
+
return const_cast<uint8_t*>(
|
|
95
|
+
static_cast<const uint8_t*>(data_->data()));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private:
|
|
99
|
+
sk_sp<SkData> data_;
|
|
100
|
+
};
|
|
101
|
+
} // namespace
|
|
102
|
+
|
|
103
|
+
static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
|
|
104
|
+
const sk_sp<SkPicture>& pic) {
|
|
105
|
+
if (!pic) return facebook::jsi::Value::null();
|
|
106
|
+
sk_sp<SkData> serialized = pic->serialize();
|
|
107
|
+
if (!serialized) return facebook::jsi::Value::null();
|
|
108
|
+
|
|
109
|
+
auto buffer = std::make_shared<SkDataMutableBuffer>(std::move(serialized));
|
|
110
|
+
jsi::ArrayBuffer arrayBuffer(rt, buffer);
|
|
111
|
+
|
|
112
|
+
auto skiaApi = rt.global().getProperty(rt, "SkiaApi");
|
|
113
|
+
if (!skiaApi.isObject()) return facebook::jsi::Value::null();
|
|
114
|
+
auto pictureFactory = skiaApi.asObject(rt).getProperty(rt, "Picture");
|
|
115
|
+
if (!pictureFactory.isObject()) return facebook::jsi::Value::null();
|
|
116
|
+
auto makePicture =
|
|
117
|
+
pictureFactory.asObject(rt).getPropertyAsFunction(rt, "MakePicture");
|
|
118
|
+
|
|
119
|
+
// Mirrors JsiSkPictureFactory::MakePicture's expected argument shape: an
|
|
120
|
+
// object with a `.buffer` property holding the ArrayBuffer (matching a
|
|
121
|
+
// Uint8Array-like value; the factory ignores everything else about it).
|
|
122
|
+
jsi::Object arg(rt);
|
|
123
|
+
arg.setProperty(rt, "buffer", arrayBuffer);
|
|
124
|
+
return makePicture.call(rt, arg);
|
|
125
|
+
}
|
|
126
|
+
#else
|
|
59
127
|
// Shared helper: wraps a fresh picture for return to JS, with memory pressure
|
|
60
128
|
// reported to Hermes so GC keeps up under gesture-rate churn.
|
|
61
129
|
static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
|
|
@@ -66,6 +134,7 @@ static facebook::jsi::Value wrapPicture(facebook::jsi::Runtime& rt,
|
|
|
66
134
|
return JSI_CREATE_HOST_OBJECT_WITH_MEMORY_PRESSURE(rt, host,
|
|
67
135
|
/*context=*/nullptr);
|
|
68
136
|
}
|
|
137
|
+
#endif
|
|
69
138
|
|
|
70
139
|
jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
71
140
|
const jsi::PropNameID& propName) {
|
|
@@ -598,6 +667,154 @@ jsi::Value ChartHostObject::get(jsi::Runtime& rt,
|
|
|
598
667
|
});
|
|
599
668
|
}
|
|
600
669
|
|
|
670
|
+
if (name == "coordAt") {
|
|
671
|
+
// coordAt(x, y) -> { timeMs, price } | null. The continuous data coordinate
|
|
672
|
+
// at a pixel — not snapped to a candle slot. Null when there are no candles
|
|
673
|
+
// or the viewport is degenerate. No rendering.
|
|
674
|
+
return jsi::Function::createFromHostFunction(
|
|
675
|
+
rt,
|
|
676
|
+
jsi::PropNameID::forAscii(rt, "coordAt"),
|
|
677
|
+
2,
|
|
678
|
+
[this](jsi::Runtime& rt2,
|
|
679
|
+
const jsi::Value& /*thisVal*/,
|
|
680
|
+
const jsi::Value* args,
|
|
681
|
+
size_t count) -> jsi::Value {
|
|
682
|
+
if (count < 2) return jsi::Value::null();
|
|
683
|
+
VroomCoord c{};
|
|
684
|
+
if (!vroom_chart_coord_at(chart_,
|
|
685
|
+
static_cast<float>(args[0].asNumber()),
|
|
686
|
+
static_cast<float>(args[1].asNumber()), &c)) {
|
|
687
|
+
return jsi::Value::null();
|
|
688
|
+
}
|
|
689
|
+
jsi::Object obj(rt2);
|
|
690
|
+
obj.setProperty(rt2, "timeMs", static_cast<double>(c.time_ms));
|
|
691
|
+
obj.setProperty(rt2, "price", c.price);
|
|
692
|
+
return obj;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (name == "setPriceLines") {
|
|
697
|
+
// setPriceLines({ lines: [{ price, color, width, lineStyle, text, quantity,
|
|
698
|
+
// flags }, ...], bodyBg, fontSizePx, lineLengthFrac, align, hoverBoost }) —
|
|
699
|
+
// replaces the full set of price status lines. No render; the next render()
|
|
700
|
+
// picks it up.
|
|
701
|
+
return jsi::Function::createFromHostFunction(
|
|
702
|
+
rt,
|
|
703
|
+
jsi::PropNameID::forAscii(rt, "setPriceLines"),
|
|
704
|
+
1,
|
|
705
|
+
[this](jsi::Runtime& rt2,
|
|
706
|
+
const jsi::Value& /*thisVal*/,
|
|
707
|
+
const jsi::Value* args,
|
|
708
|
+
size_t count) -> jsi::Value {
|
|
709
|
+
if (count < 1 || !args[0].isObject()) return jsi::Value::undefined();
|
|
710
|
+
auto cfg = args[0].asObject(rt2);
|
|
711
|
+
auto lines_val = cfg.getProperty(rt2, "lines");
|
|
712
|
+
if (!lines_val.isObject()) return jsi::Value::undefined();
|
|
713
|
+
auto lines_obj = lines_val.asObject(rt2);
|
|
714
|
+
if (!lines_obj.isArray(rt2)) return jsi::Value::undefined();
|
|
715
|
+
auto arr = lines_obj.asArray(rt2);
|
|
716
|
+
const size_t len = arr.size(rt2);
|
|
717
|
+
std::vector<VroomPriceLine> lines(len);
|
|
718
|
+
// Label storage, kept alive until set_price_lines has copied it.
|
|
719
|
+
std::vector<std::string> texts(len);
|
|
720
|
+
std::vector<std::string> quantities(len);
|
|
721
|
+
for (size_t i = 0; i < len; ++i) {
|
|
722
|
+
auto l = arr.getValueAtIndex(rt2, i).asObject(rt2);
|
|
723
|
+
lines[i].price = l.getProperty(rt2, "price").asNumber();
|
|
724
|
+
lines[i].color = static_cast<uint32_t>(
|
|
725
|
+
l.getProperty(rt2, "color").asNumber());
|
|
726
|
+
lines[i].width = static_cast<float>(
|
|
727
|
+
l.getProperty(rt2, "width").asNumber());
|
|
728
|
+
lines[i].line_style = static_cast<int32_t>(
|
|
729
|
+
l.getProperty(rt2, "lineStyle").asNumber());
|
|
730
|
+
texts[i] = l.getProperty(rt2, "text").asString(rt2).utf8(rt2);
|
|
731
|
+
quantities[i] =
|
|
732
|
+
l.getProperty(rt2, "quantity").asString(rt2).utf8(rt2);
|
|
733
|
+
lines[i].text = texts[i].c_str();
|
|
734
|
+
lines[i].quantity = quantities[i].c_str();
|
|
735
|
+
lines[i].flags = static_cast<int32_t>(
|
|
736
|
+
l.getProperty(rt2, "flags").asNumber());
|
|
737
|
+
}
|
|
738
|
+
VroomPriceLineStyle style{};
|
|
739
|
+
style.body_bg = static_cast<uint32_t>(
|
|
740
|
+
cfg.getProperty(rt2, "bodyBg").asNumber());
|
|
741
|
+
style.font_size_px = static_cast<float>(
|
|
742
|
+
cfg.getProperty(rt2, "fontSizePx").asNumber());
|
|
743
|
+
style.line_length_frac = static_cast<float>(
|
|
744
|
+
cfg.getProperty(rt2, "lineLengthFrac").asNumber());
|
|
745
|
+
style.align = static_cast<int32_t>(
|
|
746
|
+
cfg.getProperty(rt2, "align").asNumber());
|
|
747
|
+
style.hover_boost = static_cast<float>(
|
|
748
|
+
cfg.getProperty(rt2, "hoverBoost").asNumber());
|
|
749
|
+
vroom_chart_set_price_lines(chart_, lines.data(), lines.size(), &style);
|
|
750
|
+
return jsi::Value::undefined();
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (name == "hitTestPriceLine") {
|
|
755
|
+
// hitTestPriceLine(x, y) -> { index, part } | null. `part` is 0 for the line
|
|
756
|
+
// or its label body (the drag target) and 1 for the close button. Cheap
|
|
757
|
+
// enough to call at gesture rate — no rendering.
|
|
758
|
+
return jsi::Function::createFromHostFunction(
|
|
759
|
+
rt,
|
|
760
|
+
jsi::PropNameID::forAscii(rt, "hitTestPriceLine"),
|
|
761
|
+
2,
|
|
762
|
+
[this](jsi::Runtime& rt2,
|
|
763
|
+
const jsi::Value& /*thisVal*/,
|
|
764
|
+
const jsi::Value* args,
|
|
765
|
+
size_t count) -> jsi::Value {
|
|
766
|
+
if (count < 2) return jsi::Value::null();
|
|
767
|
+
int32_t index = -1, part = -1;
|
|
768
|
+
if (!vroom_chart_hit_test_price_line(
|
|
769
|
+
chart_, static_cast<float>(args[0].asNumber()),
|
|
770
|
+
static_cast<float>(args[1].asNumber()), &index, &part)) {
|
|
771
|
+
return jsi::Value::null();
|
|
772
|
+
}
|
|
773
|
+
jsi::Object obj(rt2);
|
|
774
|
+
obj.setProperty(rt2, "index", index);
|
|
775
|
+
obj.setProperty(rt2, "part", part);
|
|
776
|
+
return obj;
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (name == "setPriceLineHover") {
|
|
781
|
+
// setPriceLineHover(index, part) — highlight a price line's segment; -1
|
|
782
|
+
// clears. No hover on touch, so this exists for parity/pointer devices.
|
|
783
|
+
return jsi::Function::createFromHostFunction(
|
|
784
|
+
rt,
|
|
785
|
+
jsi::PropNameID::forAscii(rt, "setPriceLineHover"),
|
|
786
|
+
2,
|
|
787
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
788
|
+
const jsi::Value& /*thisVal*/,
|
|
789
|
+
const jsi::Value* args,
|
|
790
|
+
size_t count) -> jsi::Value {
|
|
791
|
+
if (count < 2) return jsi::Value::undefined();
|
|
792
|
+
vroom_chart_set_price_line_hover(
|
|
793
|
+
chart_, static_cast<int32_t>(args[0].asNumber()),
|
|
794
|
+
static_cast<int32_t>(args[1].asNumber()));
|
|
795
|
+
return jsi::Value::undefined();
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (name == "setPriceLineDrag") {
|
|
800
|
+
// setPriceLineDrag(index, price) — live drag preview; index -1 ends it. The
|
|
801
|
+
// committed price is untouched: restate setPriceLines to apply a move.
|
|
802
|
+
return jsi::Function::createFromHostFunction(
|
|
803
|
+
rt,
|
|
804
|
+
jsi::PropNameID::forAscii(rt, "setPriceLineDrag"),
|
|
805
|
+
2,
|
|
806
|
+
[this](jsi::Runtime& /*rt2*/,
|
|
807
|
+
const jsi::Value& /*thisVal*/,
|
|
808
|
+
const jsi::Value* args,
|
|
809
|
+
size_t count) -> jsi::Value {
|
|
810
|
+
if (count < 2) return jsi::Value::undefined();
|
|
811
|
+
vroom_chart_set_price_line_drag(
|
|
812
|
+
chart_, static_cast<int32_t>(args[0].asNumber()),
|
|
813
|
+
args[1].asNumber());
|
|
814
|
+
return jsi::Value::undefined();
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
601
818
|
if (name == "render") {
|
|
602
819
|
return jsi::Function::createFromHostFunction(
|
|
603
820
|
rt,
|