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.
@@ -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 shim (placeholder)
1
+ # Android bridge
2
2
 
3
- When native is wired up, this directory will hold the JNI bindings and a
4
- Java/Kotlin view that hosts the chart and forwards gestures into the C++ core.
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
- `build.gradle` (also to come) will compile `cpp/` + this directory + the
7
- linked `@vroomchart/core` static library via CMake.
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,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -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(24);
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,