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.
@@ -40,7 +40,17 @@ static void ensureAxisTypeface(jsi::Runtime& runtime) {
40
40
  if (!ctx) return;
41
41
  auto mgr = ctx->createFontMgr();
42
42
  if (!mgr) return;
43
- auto tf = mgr->matchFamilyStyle(nullptr, SkFontStyle()); // system default
43
+
44
+ // A null family name is meant to request "the platform default" — CoreText
45
+ // (iOS) honors that, but Android's SkFontMgr_New_Android does not: it
46
+ // returns null unless given an actual family name, even though its
47
+ // underlying font set (sans-serif, arial, ...) is perfectly populated. Fall
48
+ // back to "sans-serif" (Android's standard generic-family alias) whenever
49
+ // the null-family lookup comes back empty.
50
+ auto tf = mgr->matchFamilyStyle(nullptr, SkFontStyle());
51
+ if (!tf) {
52
+ tf = mgr->matchFamilyStyle("sans-serif", SkFontStyle());
53
+ }
44
54
  if (!tf) return;
45
55
  vroom::set_axis_typeface(tf);
46
56
  done = true;
@@ -12,13 +12,21 @@ std::shared_ptr<RNSkia::RNSkPlatformContext> getRNSkContext(
12
12
  if (!obj.isHostObject(runtime)) return nullptr;
13
13
  auto host = obj.asHostObject(runtime);
14
14
 
15
- auto base = std::dynamic_pointer_cast<RNSkia::JsiSkHostObject>(host);
16
- if (!base) return nullptr;
17
-
18
- // The accessor class adds no fields; static_cast is safe even though the
19
- // runtime type is JsiSkApi (a different JsiSkHostObject subclass). We only
20
- // call the inherited non-virtual getContext() through this view.
21
- auto* accessor = static_cast<HostObjectAccessor*>(base.get());
15
+ // `global.SkiaApi` is always installed by RN-Skia's own
16
+ // RNSkManager::installBindings() as exactly a `JsiSkApi` host object — never
17
+ // any other JsiSkHostObject subclass — so this cast is safe as a plain
18
+ // static_cast, without any RTTI check. We deliberately avoid
19
+ // dynamic_pointer_cast here: on Android, RN-Skia is compiled into its own
20
+ // librnskia.so, separate from libvroomchart.so, so a `JsiSkApi` constructed
21
+ // by librnskia.so's code carries typeinfo from that .so; a dynamic_cast
22
+ // performed here (in libvroomchart.so) sees an unmerged RTTI record for
23
+ // "the same" class and always fails, even though the object is perfectly
24
+ // valid to use. (Same underlying issue as the JsiSkPicture cross-.so cast
25
+ // documented in VroomChartHostObject.cpp — but there RN-Skia's own compiled
26
+ // code does the cast on an object *we* construct, so we can't avoid it;
27
+ // here *we* do the cast on an object *they* construct, so we can just skip
28
+ // the runtime check we don't need.)
29
+ auto* accessor = static_cast<HostObjectAccessor*>(host.get());
22
30
  return accessor->getContext();
23
31
  }
24
32
 
@@ -118,6 +118,44 @@ typedef struct VroomLiquidityStyle {
118
118
  float width_frac;
119
119
  } VroomLiquidityStyle;
120
120
 
121
+ // ---- Price status lines ---------------------------------------------------
122
+
123
+ // Bit flags for VroomPriceLine::flags.
124
+ typedef enum {
125
+ VROOM_PRICE_LINE_DRAGGABLE = 1 << 0, // the line can be dragged vertically
126
+ VROOM_PRICE_LINE_CLOSABLE = 1 << 1, // render the trailing close ("x") button
127
+ VROOM_PRICE_LINE_AXIS_LABEL = 1 << 2, // render the price badge in the y-axis strip
128
+ VROOM_PRICE_LINE_EXTEND_LEFT = 1 << 3, // extend the line to the pane's left edge
129
+ } VroomPriceLineFlags;
130
+
131
+ // A consumer-supplied horizontal status line at a fixed price — the primitive
132
+ // behind resting limit orders, take-profits, liquidation levels and the like.
133
+ //
134
+ // Visually: a line across the price pane ending in a label group made of a body
135
+ // pill (`text`), an optional solid-filled `quantity` pill, and an optional close
136
+ // button, plus an optional price badge in the y-axis strip.
137
+ //
138
+ // `text` / `quantity` are UTF-8 and copied internally, so the caller may free
139
+ // them as soon as the call returns. A null or empty string hides that segment.
140
+ typedef struct VroomPriceLine {
141
+ double price;
142
+ uint32_t color; // 0xAARRGGBB — line, border, body text, close icon
143
+ float width; // stroke width in px
144
+ int32_t line_style; // 0 = solid, 1 = dotted, 2 = dashed
145
+ const char* text; // body label
146
+ const char* quantity; // trailing solid-fill segment
147
+ int32_t flags; // bitwise-or of VroomPriceLineFlags
148
+ } VroomPriceLine;
149
+
150
+ // Layout/style shared by every price line, so the per-line struct stays small.
151
+ typedef struct VroomPriceLineStyle {
152
+ uint32_t body_bg; // translucent body/close-button pill fill (0xAARRGGBB)
153
+ float font_size_px; // 0 = inherit VROOM_FLOAT_AXIS_FONT_SIZE_PX
154
+ float line_length_frac; // 0..1 of pane width: the label group's right-edge inset
155
+ int32_t align; // 0 = left, 1 = center, 2 = right
156
+ float hover_boost; // brightness multiplier for the hovered segment (1 = flat)
157
+ } VroomPriceLineStyle;
158
+
121
159
  // A continuous data coordinate at a pixel position (no candle snapping). Used to
122
160
  // translate a drawing-tool click into a data-space anchor.
123
161
  typedef struct VroomCoord {
@@ -390,6 +428,38 @@ void vroom_chart_translate_drawing(VroomChart* chart, int32_t index,
390
428
  void vroom_chart_set_liquidity(VroomChart* chart, const VroomBand* bands,
391
429
  size_t count, const VroomLiquidityStyle* style);
392
430
 
431
+ // ---- Price status lines ---------------------------------------------------
432
+
433
+ // Replaces the full set of price status lines and their shared style. Lines
434
+ // render on the price pane above the axis labels (so their badges cover any
435
+ // label they overlap) and below the crosshair. A line whose price maps outside
436
+ // the price pane is skipped rather than clamped. Pass count 0 to clear;
437
+ // `style` may be null when count is 0.
438
+ void vroom_chart_set_price_lines(VroomChart* chart, const VroomPriceLine* lines,
439
+ size_t count,
440
+ const VroomPriceLineStyle* style);
441
+
442
+ // Hit-tests pixel (x_px, y_px) against the price lines. On a hit, fills
443
+ // *out_index with the line index and *out_part with 0 (the line or its label
444
+ // body — the drag target) or 1 (the close button), and returns true. When
445
+ // several lines are within tolerance the nearest in y wins. Only draggable
446
+ // lines report part 0 and only closable lines report part 1. Returns false on a
447
+ // miss (out params untouched). Either out pointer may be null.
448
+ bool vroom_chart_hit_test_price_line(VroomChart* chart, float x_px, float y_px,
449
+ int32_t* out_index, int32_t* out_part);
450
+
451
+ // Marks a price line's segment as hovered so it renders highlighted. `index` -1
452
+ // clears the hover. `part` matches vroom_chart_hit_test_price_line.
453
+ void vroom_chart_set_price_line_hover(VroomChart* chart, int32_t index,
454
+ int32_t part);
455
+
456
+ // Drives the live drag preview: the line, its label and its badge render at
457
+ // `price` instead of the committed one, and a faint ghost marks where it
458
+ // started. `index` -1 ends the preview. The committed price is never mutated —
459
+ // the host applies (or rejects) the new price by restating its lines.
460
+ void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index,
461
+ double price);
462
+
393
463
  // Sets the transient in-progress "draft" the drawing tool shows while the user
394
464
  // places points. Node A is always shown; when `has_b`, node B is shown too.
395
465
  // `guide != 0` also draws the live preview (a guideline for a line, or a preview
@@ -30,6 +30,7 @@
30
30
  #include "macd.h"
31
31
  #include "macd_pane.h"
32
32
  #include "price_indicator.h"
33
+ #include "price_lines.h"
33
34
  #include "rsi.h"
34
35
  #include "rsi_pane.h"
35
36
  #include "volume.h"
@@ -279,6 +280,12 @@ void VroomChart::draw_chart(SkCanvas* canvas) {
279
280
  vroom::price_indicator::draw(canvas, *this, lay, bounds,
280
281
  candle_right, candle_area_h);
281
282
 
283
+ // 7.55. Consumer-supplied price status lines — same tier as the current-price
284
+ // indicator (their badges must cover the labels underneath), but after
285
+ // it so a resting order at the last close stays readable.
286
+ vroom::price_lines::draw(canvas, *this, lay, bounds, candle_right,
287
+ candle_area_h);
288
+
282
289
  // 7.6. Indicator panes stacked below the candles, ordered by enable
283
290
  // sequence (most recently enabled at the bottom). Each pane is
284
291
  // INDICATOR_HEIGHT_FRAC of the height; the candle pane already shrank
@@ -9,6 +9,7 @@
9
9
  #pragma once
10
10
 
11
11
  #include <chrono>
12
+ #include <string>
12
13
  #include <vector>
13
14
 
14
15
  #include "vroom/vroom_chart.h"
@@ -195,6 +196,32 @@ struct VroomChart {
195
196
  std::vector<VroomBand> bands;
196
197
  VroomLiquidityStyle liquidity_style{};
197
198
 
199
+ // --- price status lines -------------------------------------------------
200
+ // Consumer-supplied horizontal lines at fixed prices (resting orders, TP/SL,
201
+ // liquidation levels), each with a label group and an optional close button.
202
+ //
203
+ // Mirrors the public VroomPriceLine but *owns* its label strings, so the
204
+ // caller may free theirs as soon as the setter returns.
205
+ struct StoredPriceLine {
206
+ double price = 0.0;
207
+ uint32_t color = 0xffef5350;
208
+ float width = 1.f;
209
+ int32_t line_style = 1; // dotted, matching the price indicator
210
+ std::string text;
211
+ std::string quantity;
212
+ int32_t flags = 0; // VroomPriceLineFlags
213
+ };
214
+ std::vector<StoredPriceLine> price_lines;
215
+ VroomPriceLineStyle price_line_style{};
216
+
217
+ // Interaction state, driven by the host's gesture layer. The hovered segment
218
+ // renders highlighted; while a line is dragged it renders at
219
+ // dragged_price_line_price with a ghost at its committed price. -1 = none.
220
+ int32_t hovered_price_line = -1;
221
+ int32_t hovered_price_line_part = -1;
222
+ int32_t dragged_price_line = -1;
223
+ double dragged_price_line_price = 0.0;
224
+
198
225
  // --- theme --------------------------------------------------------------
199
226
  vroom::Theme theme;
200
227
 
@@ -14,6 +14,7 @@
14
14
  #include "chart.h"
15
15
  #include "drawings.h"
16
16
  #include "labels.h"
17
+ #include "price_lines.h"
17
18
  #include "viewport.h"
18
19
 
19
20
  class SkCanvas;
@@ -812,6 +813,70 @@ extern "C" void vroom_chart_set_liquidity(VroomChart* chart,
812
813
  chart->mark_dirty();
813
814
  }
814
815
 
816
+ // ---- Price status lines ---------------------------------------------------
817
+
818
+ extern "C" void vroom_chart_set_price_lines(VroomChart* chart,
819
+ const VroomPriceLine* lines,
820
+ size_t count,
821
+ const VroomPriceLineStyle* style) {
822
+ if (!chart) return;
823
+ // Deep-copy into the owning form: the label strings live in the chart, so the
824
+ // caller's buffers may be freed as soon as this returns.
825
+ chart->price_lines.clear();
826
+ chart->price_lines.reserve(count);
827
+ for (size_t i = 0; i < count; ++i) {
828
+ const VroomPriceLine& src = lines[i];
829
+ VroomChart::StoredPriceLine pl;
830
+ pl.price = src.price;
831
+ pl.color = src.color;
832
+ pl.width = src.width;
833
+ pl.line_style = src.line_style;
834
+ if (src.text) pl.text = src.text;
835
+ if (src.quantity) pl.quantity = src.quantity;
836
+ pl.flags = src.flags;
837
+ chart->price_lines.push_back(std::move(pl));
838
+ }
839
+ if (style) chart->price_line_style = *style;
840
+ // Drop interaction state that no longer refers to a live line — otherwise a
841
+ // removed line would leave a stuck hover highlight or drag preview.
842
+ const auto n = static_cast<int32_t>(count);
843
+ if (chart->hovered_price_line >= n) {
844
+ chart->hovered_price_line = -1;
845
+ chart->hovered_price_line_part = -1;
846
+ }
847
+ if (chart->dragged_price_line >= n) chart->dragged_price_line = -1;
848
+ chart->mark_dirty();
849
+ }
850
+
851
+ extern "C" void vroom_chart_set_price_line_hover(VroomChart* chart, int32_t index,
852
+ int32_t part) {
853
+ if (!chart) return;
854
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
855
+ index = -1;
856
+ part = -1;
857
+ }
858
+ if (chart->hovered_price_line == index && chart->hovered_price_line_part == part) {
859
+ return; // hover fires on every pointer move; don't redraw for nothing
860
+ }
861
+ chart->hovered_price_line = index;
862
+ chart->hovered_price_line_part = part;
863
+ chart->mark_dirty();
864
+ }
865
+
866
+ extern "C" void vroom_chart_set_price_line_drag(VroomChart* chart, int32_t index,
867
+ double price) {
868
+ if (!chart) return;
869
+ if (index < 0 || index >= static_cast<int32_t>(chart->price_lines.size())) {
870
+ if (chart->dragged_price_line == -1) return;
871
+ chart->dragged_price_line = -1;
872
+ chart->mark_dirty();
873
+ return;
874
+ }
875
+ chart->dragged_price_line = index;
876
+ chart->dragged_price_line_price = price;
877
+ chart->mark_dirty();
878
+ }
879
+
815
880
  extern "C" void vroom_chart_set_draft(VroomChart* chart, int64_t a_time,
816
881
  double a_price, bool has_b, int64_t b_time,
817
882
  double b_price, bool guide, uint32_t color,
@@ -926,6 +991,32 @@ extern "C" bool vroom_chart_hit_test_drawing(VroomChart* chart, float x_px,
926
991
  return true;
927
992
  }
928
993
 
994
+ extern "C" bool vroom_chart_hit_test_price_line(VroomChart* chart, float x_px,
995
+ float y_px, int32_t* out_index,
996
+ int32_t* out_part) {
997
+ if (!chart || chart->price_lines.empty() || chart->candles.empty()) return false;
998
+ const auto lay = chart->layout();
999
+ // Same bounds and pane geometry as draw_chart, so the grab regions line up
1000
+ // with what's on screen.
1001
+ const auto range = vroom::visible_indices(
1002
+ chart->candles.data(), chart->candles.size(),
1003
+ chart->visible_start_ms, chart->visible_end_ms);
1004
+ const size_t n = range.end - range.start;
1005
+ const auto bounds =
1006
+ chart->price_bounds_manual
1007
+ ? chart->price_bounds
1008
+ : vroom::auto_price_bounds(chart->candles.data() + range.start, n);
1009
+ const float candle_area_h = vroom::price_pane_bottom(lay);
1010
+ const float candle_right =
1011
+ chart->width_px - lay.y_axis_width_px - lay.right_padding_px;
1012
+ const auto hit = vroom::price_lines::hit_test(*chart, lay, bounds, candle_right,
1013
+ candle_area_h, x_px, y_px);
1014
+ if (hit.index < 0) return false;
1015
+ if (out_index) *out_index = hit.index;
1016
+ if (out_part) *out_part = hit.part;
1017
+ return true;
1018
+ }
1019
+
929
1020
  extern "C" void vroom_chart_set_selected_drawing(VroomChart* chart,
930
1021
  int32_t index,
931
1022
  int32_t grabbed_endpoint) {
@@ -0,0 +1,87 @@
1
+ #include "price_line_layout.h"
2
+
3
+ #include <algorithm>
4
+ #include <cmath>
5
+
6
+ namespace vroom::price_lines {
7
+
8
+ namespace {
9
+ // A pill's width is its text plus symmetric padding. Zero-width text means the
10
+ // segment is absent, not a padding-only sliver.
11
+ float pill_width(float text_w) {
12
+ return text_w > 0.f ? text_w + 2.f * kPadH : 0.f;
13
+ }
14
+ } // namespace
15
+
16
+ bool contains(const Rect& r, float x, float y) {
17
+ if (r.empty()) return false;
18
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
19
+ }
20
+
21
+ GroupLayout layout_group(const LabelMetrics& metrics,
22
+ float y,
23
+ float pane_right,
24
+ const ::VroomPriceLineStyle& style) {
25
+ GroupLayout out;
26
+
27
+ const float body_w = pill_width(metrics.text_w);
28
+ const float qty_w = pill_width(metrics.quantity_w);
29
+ // The close button is a square cell, so its icon stays centered whatever the
30
+ // font size works out to.
31
+ const float close_w = metrics.closable ? metrics.label_h : 0.f;
32
+ const float group_w = body_w + qty_w + close_w;
33
+
34
+ if (group_w <= 0.f || metrics.label_h <= 0.f || pane_right <= 0.f) {
35
+ // A bare line: no group, but still report a degenerate span at the
36
+ // anchor so callers can treat left/right uniformly.
37
+ out.left = out.right = pane_right;
38
+ return out;
39
+ }
40
+
41
+ float left = 0.f;
42
+ switch (style.align) {
43
+ case 0: // left
44
+ left = 0.f;
45
+ break;
46
+ case 1: // center
47
+ left = (pane_right - group_w) * 0.5f;
48
+ break;
49
+ default: { // right
50
+ const float frac = std::clamp(style.line_length_frac, 0.f, 1.f);
51
+ const float right = pane_right - frac * pane_right - kAxisGutter;
52
+ left = right - group_w;
53
+ break;
54
+ }
55
+ }
56
+ // Keep the group on-screen: never past the axis, never off the left edge.
57
+ left = std::min(left, pane_right - group_w);
58
+ left = std::max(left, 0.f);
59
+
60
+ const float top = y - metrics.label_h * 0.5f;
61
+ const float bottom = top + metrics.label_h;
62
+
63
+ float x = left;
64
+ if (body_w > 0.f) {
65
+ out.body = Rect{x, top, x + body_w, bottom};
66
+ x += body_w;
67
+ }
68
+ if (qty_w > 0.f) {
69
+ out.quantity = Rect{x, top, x + qty_w, bottom};
70
+ x += qty_w;
71
+ }
72
+ if (close_w > 0.f) {
73
+ out.close = Rect{x, top, x + close_w, bottom};
74
+ x += close_w;
75
+ }
76
+
77
+ out.left = left;
78
+ out.right = x;
79
+ return out;
80
+ }
81
+
82
+ bool hits_line(float line_y, float stroke_width, float y) {
83
+ const float band = std::max(stroke_width, 1.f) + kLineHitTolerance;
84
+ return std::fabs(y - line_y) <= band;
85
+ }
86
+
87
+ } // namespace vroom::price_lines
@@ -0,0 +1,80 @@
1
+ // Price-status-line geometry — pure functions, no Skia, no state.
2
+ //
3
+ // Split out from price_lines.cpp so the rendering pass and the hit-test pass
4
+ // derive their rectangles from the same arithmetic: a label the user can see but
5
+ // not click (or vice versa) is the classic failure mode for this widget. Being
6
+ // Skia-free it also unit-tests without a Skia checkout.
7
+
8
+ #pragma once
9
+
10
+ #include "vroom/vroom_chart.h"
11
+ namespace vroom::price_lines {
12
+
13
+ // Pill padding / corner radius, matching the current-price indicator so a price
14
+ // line and the price badge read as the same family of chrome.
15
+ constexpr float kPadH = 8.f; // left/right of the text
16
+ constexpr float kPadV = 4.f; // above/below the text
17
+ constexpr float kCorner = 6.f; // rounded-pill corner radius
18
+
19
+ // Extra grab band above and below the stroke, so a 1px line is still an easy
20
+ // target. Matches the tolerance TradingView's own chart libraries settled on.
21
+ constexpr float kLineHitTolerance = 7.f;
22
+
23
+ // Gap between the label group and the price axis when right-aligned. Wide enough
24
+ // that the line still shows through it — a couple of dashes reading as "this
25
+ // continues to the badge" rather than one orphaned dot.
26
+ constexpr float kAxisGutter = 12.f;
27
+
28
+ struct Rect {
29
+ float left = 0.f;
30
+ float top = 0.f;
31
+ float right = 0.f;
32
+ float bottom = 0.f;
33
+
34
+ // Empty rects stand in for absent segments (no quantity, no close button).
35
+ bool empty() const { return right <= left || bottom <= top; }
36
+ float width() const { return right - left; }
37
+ };
38
+
39
+ // True when (x, y) falls inside `r`. Always false for an empty rect.
40
+ bool contains(const Rect& r, float x, float y);
41
+
42
+ // The text extents a caller measured with the real font, plus which optional
43
+ // segments to reserve room for. `label_h` is the pill height (already including
44
+ // kPadV on both sides).
45
+ struct LabelMetrics {
46
+ float text_w = 0.f; // body text width; 0 hides the body pill
47
+ float quantity_w = 0.f; // quantity text width; 0 hides the quantity pill
48
+ float label_h = 0.f;
49
+ bool closable = false; // reserve the trailing square close-button cell
50
+ };
51
+
52
+ // The laid-out label group, vertically centered on the line. Any of the three
53
+ // segment rects may be empty. `left`/`right` bound whichever segments exist and
54
+ // are equal when the group is empty (a bare line).
55
+ struct GroupLayout {
56
+ Rect body;
57
+ Rect quantity;
58
+ Rect close;
59
+ float left = 0.f;
60
+ float right = 0.f;
61
+
62
+ bool empty() const { return right <= left; }
63
+ };
64
+
65
+ // Lays the group out on the line at `y`. `pane_right` is the inner edge of the
66
+ // price axis (the line's right end).
67
+ //
68
+ // `style.align` picks the anchor: 2 (right, the default) parks the group's right
69
+ // edge `style.line_length_frac` of the pane width in from the axis; 1 centers
70
+ // it; 0 pins it to the left edge. The group is clamped into [0, pane_right], so
71
+ // a label wider than the pane starts flush left rather than overflowing.
72
+ GroupLayout layout_group(const LabelMetrics& metrics,
73
+ float y,
74
+ float pane_right,
75
+ const ::VroomPriceLineStyle& style);
76
+
77
+ // True when `y` is within the grab band of a line drawn at `line_y`.
78
+ bool hits_line(float line_y, float stroke_width, float y);
79
+
80
+ } // namespace vroom::price_lines