skia-canvas 0.1.0

GPU-accelerated, multi-threaded HTML Canvas-compatible 2D rendering for Rust and Node, powered by Skia.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
//
// ParagraphBuilder & Paragraph wrappers for CanvasKit parity
//
#![allow(non_snake_case)]
use neon::prelude::*;
use std::cell::RefCell;

use skia_safe::{
    Color, ColorSpace, FourByteTag, Paint, Point,
    font_style::{FontStyle, Slant, Weight, Width},
    textlayout::{
        FontCollection, Paragraph as SkParagraph, ParagraphBuilder as SkParagraphBuilder,
        ParagraphStyle, PlaceholderStyle, RectHeightStyle, RectWidthStyle, TextAlign,
        TextDecoration, TextDecorationMode, TextDecorationStyle, TextDirection, TextShadow,
        TextStyle,
    },
};

use crate::{font_library::FontLibrary, utils::*};

//
// Boxed wrapper types
//

pub struct ParagraphBuilderWrap {
    builder: Option<SkParagraphBuilder>,
    _collection: FontCollection,
}

impl Finalize for ParagraphBuilderWrap {}
pub type BoxedParagraphBuilder = JsBox<RefCell<ParagraphBuilderWrap>>;

pub struct ParagraphWrap {
    pub paragraph: SkParagraph,
}

impl Finalize for ParagraphWrap {}
pub type BoxedParagraph = JsBox<RefCell<ParagraphWrap>>;

//
// Style parsing helpers
//

/// Parse `fontVariations: [{axis, value}]` from a TextStyleInput JS
/// object. Axis tags must be exactly 4 ASCII characters (Skia
/// convention, e.g. "wght", "wdth", "ital"). Malformed entries are
/// silently skipped -- the field is optional and missing variations
/// fall back to the typeface's default axis positions.
fn parse_font_variations(
    cx: &mut FunctionContext,
    obj: &Handle<JsObject>,
) -> NeonResult<Vec<(FourByteTag, f32)>> {
    let mut out: Vec<(FourByteTag, f32)> = Vec::new();
    let Ok(vars_val) = obj.get::<JsValue, _, _>(cx, "fontVariations") else {
        return Ok(out);
    };
    let Ok(vars_arr) = vars_val.downcast::<JsArray, _>(cx) else {
        return Ok(out);
    };
    for v in vars_arr.to_vec(cx)? {
        let Ok(v_obj) = v.downcast::<JsObject, _>(cx) else {
            continue;
        };
        let Some(axis) = opt_string_for_key(cx, &v_obj, "axis") else {
            continue;
        };
        let Some(value) = opt_float_for_key(cx, &v_obj, "value") else {
            continue;
        };
        let bytes = axis.as_bytes();
        if bytes.len() != 4 {
            continue;
        }
        let tag = FourByteTag::from_chars(
            bytes[0] as char,
            bytes[1] as char,
            bytes[2] as char,
            bytes[3] as char,
        );
        out.push((tag, value));
    }
    Ok(out)
}

fn parse_text_style(cx: &mut FunctionContext, obj: &Handle<JsObject>) -> NeonResult<TextStyle> {
    let mut style = TextStyle::new();

    // fontSize
    if let Some(size) = opt_float_for_key(cx, obj, "fontSize") {
        style.set_font_size(size);
    }

    // fontFamilies
    if let Ok(fam_val) = obj.get::<JsValue, _, _>(cx, "fontFamilies")
        && let Ok(fam_arr) = fam_val.downcast::<JsArray, _>(cx)
    {
        let fam_vec = fam_arr.to_vec(cx)?;
        let families = strings_in(cx, &fam_vec);
        style.set_font_families(&families);
    }

    // color / foregroundColor / backgroundColor accept either:
    //   * a CSS string -- tagged as sRGB by `color4f_in`,
    //   * a `[r, g, b, a]` float array -- tagged here as `srgb_linear` so
    //     Skia converts to the destination working color space at paint
    //     time instead of treating the linear values as sRGB-encoded.
    if let Ok(color_val) = obj.get::<JsValue, _, _>(cx, "color")
        && let Some((color4f, cs)) = color4f_in(cx, color_val)
    {
        let mut paint = Paint::default();
        let cs = cs.unwrap_or_else(ColorSpace::new_srgb_linear);
        paint.set_color4f(color4f, Some(&cs));
        style.set_foreground_paint(&paint);
    }
    if let Ok(color_val) = obj.get::<JsValue, _, _>(cx, "foregroundColor")
        && let Some((color4f, cs)) = color4f_in(cx, color_val)
    {
        let mut paint = Paint::default();
        let cs = cs.unwrap_or_else(ColorSpace::new_srgb_linear);
        paint.set_color4f(color4f, Some(&cs));
        style.set_foreground_paint(&paint);
    }

    // backgroundColor
    if let Ok(color_val) = obj.get::<JsValue, _, _>(cx, "backgroundColor")
        && let Some((color4f, cs)) = color4f_in(cx, color_val)
    {
        let mut paint = Paint::default();
        let cs = cs.unwrap_or_else(ColorSpace::new_srgb_linear);
        paint.set_color4f(color4f, Some(&cs));
        style.set_background_paint(&paint);
    }

    // fontStyle: { weight, width, slant }
    if let Ok(fs_val) = obj.get::<JsValue, _, _>(cx, "fontStyle")
        && let Ok(fs_obj) = fs_val.downcast::<JsObject, _>(cx)
    {
        let weight = opt_float_for_key(cx, &fs_obj, "weight")
            .map(|w| Weight::from(w as i32))
            .unwrap_or(Weight::NORMAL);
        let width = opt_float_for_key(cx, &fs_obj, "width")
            .map(|w| Width::from(w as i32))
            .unwrap_or(Width::NORMAL);
        let slant = opt_float_for_key(cx, &fs_obj, "slant")
            .map(|s| match s as i32 {
                1 => Slant::Italic,
                2 => Slant::Oblique,
                _ => Slant::Upright,
            })
            .unwrap_or(Slant::Upright);
        style.set_font_style(FontStyle::new(weight, width, slant));
    }

    // letterSpacing
    if let Some(ls) = opt_float_for_key(cx, obj, "letterSpacing") {
        style.set_letter_spacing(ls);
    }

    // wordSpacing
    if let Some(ws) = opt_float_for_key(cx, obj, "wordSpacing") {
        style.set_word_spacing(ws);
    }

    // heightMultiplier
    if let Some(hm) = opt_float_for_key(cx, obj, "heightMultiplier") {
        style.set_height(hm);
        style.set_height_override(true);
    }

    // decoration (bitmask)
    if let Some(deco) = opt_float_for_key(cx, obj, "decoration") {
        let deco_val = deco as u32;
        let mut td = TextDecoration::NO_DECORATION;
        if deco_val & 0x1 != 0 {
            td |= TextDecoration::UNDERLINE;
        }
        if deco_val & 0x2 != 0 {
            td |= TextDecoration::OVERLINE;
        }
        if deco_val & 0x4 != 0 {
            td |= TextDecoration::LINE_THROUGH;
        }
        style.set_decoration_type(td);
    }

    // decorationStyle
    if let Some(ds) = opt_float_for_key(cx, obj, "decorationStyle") {
        style.set_decoration_style(match ds as i32 {
            1 => TextDecorationStyle::Double,
            2 => TextDecorationStyle::Dotted,
            3 => TextDecorationStyle::Dashed,
            4 => TextDecorationStyle::Wavy,
            _ => TextDecorationStyle::Solid,
        });
    }

    // decorationColor accepts a CSS string or a `[r, g, b, a]` linear float
    // array. Skia's `set_decoration_color` takes a u32 ARGB tag that it
    // treats as sRGB-encoded, so the linear-array path must gamma-encode
    // before quantizing -- otherwise Skia's implicit decode darkens the
    // decoration and drops the alpha precision the caller asked for.
    if let Ok(color_val) = obj.get::<JsValue, _, _>(cx, "decorationColor")
        && let Some((color4f, cs)) = color4f_in(cx, color_val)
    {
        let sk_color = if cs.is_some() {
            color4f.to_color()
        } else {
            linear_color4f_to_srgb_color(&color4f)
        };
        style.set_decoration_color(sk_color);
    }

    // decorationThickness
    if let Some(dt) = opt_float_for_key(cx, obj, "decorationThickness") {
        style.set_decoration_thickness_multiplier(dt);
    }

    // decoration mode
    style.set_decoration_mode(TextDecorationMode::Through);

    // NOTE: `fontVariations` is parsed in `paragraph::new` instead of
    // here. `SkParagraphBuilder` reads its font collection at
    // construction time, not per-`pushStyle`, so the instantiated
    // variable typeface has to be seeded on the builder's collection.
    // A `style.set_typeface(face)` call here would be ignored by the
    // layout path (the collection-resident match takes precedence on
    // every glyph run), so we deliberately don't attempt one.

    // shadows: [{ color, offset: [dx, dy], blurRadius }]
    if let Ok(shadows_val) = obj.get::<JsValue, _, _>(cx, "shadows")
        && let Ok(shadows_arr) = shadows_val.downcast::<JsArray, _>(cx)
    {
        for shadow_val in shadows_arr.to_vec(cx)? {
            if let Ok(shadow_obj) = shadow_val.downcast::<JsObject, _>(cx) {
                // Shadow color follows the same rule as decorationColor:
                // CSS strings round-trip through u32 ARGB cleanly, linear
                // float arrays must be gamma-encoded first.
                let color = shadow_obj
                    .get::<JsValue, _, _>(cx, "color")
                    .ok()
                    .and_then(|v| color4f_in(cx, v))
                    .map(|(c, cs)| {
                        if cs.is_some() {
                            c.to_color()
                        } else {
                            linear_color4f_to_srgb_color(&c)
                        }
                    })
                    .unwrap_or(Color::BLACK);

                let mut offset = Point::new(0.0, 0.0);
                if let Ok(offset_val) = shadow_obj.get::<JsValue, _, _>(cx, "offset")
                    && let Ok(offset_arr) = offset_val.downcast::<JsArray, _>(cx)
                {
                    let vals = offset_arr.to_vec(cx)?;
                    if vals.len() >= 2
                        && let (Ok(dx), Ok(dy)) = (
                            vals[0].downcast::<JsNumber, _>(cx),
                            vals[1].downcast::<JsNumber, _>(cx),
                        )
                    {
                        offset = Point::new(dx.value(cx) as f32, dy.value(cx) as f32);
                    }
                }

                let blur = opt_float_for_key(cx, &shadow_obj, "blurRadius").unwrap_or(0.0);

                style.add_shadow(TextShadow::new(color, offset, blur as f64));
            }
        }
    }

    Ok(style)
}

fn parse_paragraph_style(
    cx: &mut FunctionContext,
    obj: &Handle<JsObject>,
) -> NeonResult<ParagraphStyle> {
    let mut style = ParagraphStyle::new();

    // textAlign
    if let Some(align_str) = opt_string_for_key(cx, obj, "textAlign") {
        match align_str.to_lowercase().as_str() {
            "left" => {
                style.set_text_align(TextAlign::Left);
            }
            "right" => {
                style.set_text_align(TextAlign::Right);
            }
            "center" => {
                style.set_text_align(TextAlign::Center);
            }
            "justify" => {
                style.set_text_align(TextAlign::Justify);
            }
            "start" => {
                style.set_text_align(TextAlign::Start);
            }
            "end" => {
                style.set_text_align(TextAlign::End);
            }
            _ => {}
        }
    }

    // textDirection
    if let Some(dir_str) = opt_string_for_key(cx, obj, "textDirection") {
        match dir_str.to_lowercase().as_str() {
            "rtl" => {
                style.set_text_direction(TextDirection::RTL);
            }
            "ltr" => {
                style.set_text_direction(TextDirection::LTR);
            }
            _ => {}
        }
    }

    // maxLines
    if let Some(max) = opt_float_for_key(cx, obj, "maxLines") {
        style.set_max_lines(Some(max as usize));
    }

    // ellipsis
    if let Some(ell) = opt_string_for_key(cx, obj, "ellipsis")
        && !ell.is_empty()
    {
        style.set_ellipsis(ell);
    }

    // textStyle (default text style for the paragraph)
    if let Ok(ts_val) = obj.get::<JsValue, _, _>(cx, "textStyle")
        && let Ok(ts_obj) = ts_val.downcast::<JsObject, _>(cx)
    {
        let text_style = parse_text_style(cx, &ts_obj)?;
        style.set_text_style(&text_style);
    }

    Ok(style)
}

/// Parse the `textStyle.fontVariations` array from a ParagraphStyleInput
/// JS object, mirroring `parse_font_variations` in shape. Returned to
/// `paragraph::new` so the font collection used for typeface matching
/// can be the variable-instantiated one (otherwise the paragraph
/// engine renders at the typeface master regardless of explicit weight,
/// breaking parity with CanvasKit's behaviour on variable fonts).
fn parse_paragraph_font_variations(
    cx: &mut FunctionContext,
    obj: &Handle<JsObject>,
) -> NeonResult<Vec<(FourByteTag, f32)>> {
    let Ok(ts_val) = obj.get::<JsValue, _, _>(cx, "textStyle") else {
        return Ok(Vec::new());
    };
    let Ok(ts_obj) = ts_val.downcast::<JsObject, _>(cx) else {
        return Ok(Vec::new());
    };
    parse_font_variations(cx, &ts_obj)
}

//
// ParagraphBuilder FFI functions
//

pub fn new(mut cx: FunctionContext) -> JsResult<BoxedParagraphBuilder> {
    let style_arg = cx.argument::<JsObject>(1)?;
    let para_style = parse_paragraph_style(&mut cx, &style_arg)?;
    let variations = parse_paragraph_font_variations(&mut cx, &style_arg)?;

    // Route through `fonts_for_style` so any variable typefaces in the
    // collection get instantiated at the requested axis positions (or
    // auto-`wght` from the text style's nominal weight when no
    // explicit variations are passed). Without this the paragraph
    // engine matches the typeface's master instance regardless of
    // weight and the rendered glyphs drift off the requested wght
    // axis -- a visible parity gap on variable fonts like Dosis vs
    // CanvasKit's render.
    let text_style = para_style.text_style().clone();
    let collection = FontLibrary::with_shared(|lib| lib.fonts_for_style(&text_style, &variations));

    let builder = SkParagraphBuilder::new(&para_style, &collection);

    Ok(cx.boxed(RefCell::new(ParagraphBuilderWrap {
        builder: Some(builder),
        _collection: collection,
    })))
}

pub fn pushStyle(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.argument::<BoxedParagraphBuilder>(0)?;
    let style_obj = cx.argument::<JsObject>(1)?;
    let text_style = parse_text_style(&mut cx, &style_obj)?;

    let mut this = this.borrow_mut();
    if let Some(builder) = this.builder.as_mut() {
        builder.push_style(&text_style);
    }
    Ok(cx.undefined())
}

pub fn pop(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.argument::<BoxedParagraphBuilder>(0)?;
    let mut this = this.borrow_mut();
    if let Some(builder) = this.builder.as_mut() {
        builder.pop();
    }
    Ok(cx.undefined())
}

pub fn addText(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.argument::<BoxedParagraphBuilder>(0)?;
    let text = string_arg(&mut cx, 1, "text")?;

    let mut this = this.borrow_mut();
    if let Some(builder) = this.builder.as_mut() {
        builder.add_text(&text);
    }
    Ok(cx.undefined())
}

pub fn addPlaceholder(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.argument::<BoxedParagraphBuilder>(0)?;
    let width = float_arg_or_bail(&mut cx, 1, "width")?;
    let height = float_arg_or_bail(&mut cx, 2, "height")?;
    let _align = opt_float_arg(&mut cx, 3).unwrap_or(0.0);
    let _baseline = opt_float_arg(&mut cx, 4).unwrap_or(0.0);
    let offset = opt_float_arg(&mut cx, 5).unwrap_or(0.0);

    // Use default alignment and baseline for simplicity
    let placeholder = PlaceholderStyle::default();
    let placeholder = PlaceholderStyle {
        width,
        height,
        baseline_offset: offset,
        ..placeholder
    };

    let mut this = this.borrow_mut();
    if let Some(builder) = this.builder.as_mut() {
        builder.add_placeholder(&placeholder);
    }
    Ok(cx.undefined())
}

pub fn build(mut cx: FunctionContext) -> JsResult<BoxedParagraph> {
    let this = cx.argument::<BoxedParagraphBuilder>(0)?;
    let mut this = this.borrow_mut();

    match this.builder.take() {
        Some(mut builder) => {
            let paragraph = builder.build();
            Ok(cx.boxed(RefCell::new(ParagraphWrap { paragraph })))
        }
        None => cx.throw_error("ParagraphBuilder has already been consumed by build()"),
    }
}

//
// Paragraph FFI functions
//

pub fn layout(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let width = float_arg_or_bail(&mut cx, 1, "width")?;

    let mut this = this.borrow_mut();
    this.paragraph.layout(width);
    Ok(cx.undefined())
}

pub fn paint(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    // This is handled via drawParagraph on the context side
    cx.throw_error("Use ctx.drawParagraph() instead of Paragraph.paint()")
}

pub fn getHeight(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.height()))
}

pub fn getLongestLine(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.longest_line()))
}

pub fn getMaxWidth(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.max_width()))
}

pub fn getMaxIntrinsicWidth(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.max_intrinsic_width()))
}

pub fn getMinIntrinsicWidth(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.min_intrinsic_width()))
}

pub fn getAlphabeticBaseline(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.alphabetic_baseline()))
}

pub fn getIdeographicBaseline(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();
    Ok(cx.number(this.paragraph.ideographic_baseline()))
}

pub fn getLineMetrics(mut cx: FunctionContext) -> JsResult<JsArray> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let this = this.borrow();

    let metrics = this.paragraph.get_line_metrics();
    let result = JsArray::new(&mut cx, metrics.len());

    for (i, m) in metrics.iter().enumerate() {
        let obj = JsObject::new(&mut cx);

        let v = cx.number(m.start_index as f64);
        obj.set(&mut cx, "startIndex", v)?;
        let v = cx.number(m.end_index as f64);
        obj.set(&mut cx, "endIndex", v)?;
        let v = cx.number(m.end_excluding_whitespaces as f64);
        obj.set(&mut cx, "endExcludingWhitespaces", v)?;
        let v = cx.number(m.end_including_newline as f64);
        obj.set(&mut cx, "endIncludingNewline", v)?;
        let v = cx.boolean(m.hard_break);
        obj.set(&mut cx, "isHardBreak", v)?;
        let v = cx.number(m.ascent);
        obj.set(&mut cx, "ascent", v)?;
        let v = cx.number(m.descent);
        obj.set(&mut cx, "descent", v)?;
        let v = cx.number(m.height);
        obj.set(&mut cx, "height", v)?;
        let v = cx.number(m.width);
        obj.set(&mut cx, "width", v)?;
        let v = cx.number(m.left);
        obj.set(&mut cx, "left", v)?;
        let v = cx.number(m.baseline);
        obj.set(&mut cx, "baseline", v)?;
        let v = cx.number(m.line_number as f64);
        obj.set(&mut cx, "lineNumber", v)?;

        result.set(&mut cx, i as u32, obj)?;
    }

    Ok(result)
}

pub fn getGlyphPositionAtCoordinate(mut cx: FunctionContext) -> JsResult<JsObject> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let x = float_arg_or_bail(&mut cx, 1, "x")?;
    let y = float_arg_or_bail(&mut cx, 2, "y")?;

    let this = this.borrow();
    let pos = this.paragraph.get_glyph_position_at_coordinate((x, y));

    let result = JsObject::new(&mut cx);
    let pos_val = cx.number(pos.position as f64);
    result.set(&mut cx, "pos", pos_val)?;
    let affinity_val = cx.number(pos.affinity as i32 as f64);
    result.set(&mut cx, "affinity", affinity_val)?;

    Ok(result)
}

pub fn getRectsForRange(mut cx: FunctionContext) -> JsResult<JsArray> {
    let this = cx.argument::<BoxedParagraph>(0)?;
    let start = float_arg_or_bail(&mut cx, 1, "start")? as usize;
    let end = float_arg_or_bail(&mut cx, 2, "end")? as usize;
    let h_style = opt_float_arg(&mut cx, 3).unwrap_or(0.0) as i32;
    let w_style = opt_float_arg(&mut cx, 4).unwrap_or(0.0) as i32;

    let rect_height_style = match h_style {
        1 => RectHeightStyle::Max,
        2 => RectHeightStyle::IncludeLineSpacingMiddle,
        3 => RectHeightStyle::IncludeLineSpacingTop,
        4 => RectHeightStyle::IncludeLineSpacingBottom,
        5 => RectHeightStyle::Strut,
        _ => RectHeightStyle::Tight,
    };
    let rect_width_style = match w_style {
        1 => RectWidthStyle::Max,
        _ => RectWidthStyle::Tight,
    };

    let this = this.borrow();
    let boxes = this
        .paragraph
        .get_rects_for_range(start..end, rect_height_style, rect_width_style);

    let result = JsArray::new(&mut cx, boxes.len());
    for (i, tb) in boxes.iter().enumerate() {
        let obj = JsObject::new(&mut cx);

        let rect = JsArray::new(&mut cx, 4);
        let v = cx.number(tb.rect.left);
        rect.set(&mut cx, 0u32, v)?;
        let v = cx.number(tb.rect.top);
        rect.set(&mut cx, 1u32, v)?;
        let v = cx.number(tb.rect.right);
        rect.set(&mut cx, 2u32, v)?;
        let v = cx.number(tb.rect.bottom);
        rect.set(&mut cx, 3u32, v)?;
        obj.set(&mut cx, "rect", rect)?;

        let dir = cx.number(tb.direct as i32 as f64);
        obj.set(&mut cx, "direction", dir)?;

        result.set(&mut cx, i as u32, obj)?;
    }

    Ok(result)
}

//
// Context integration: drawParagraph
//

pub fn drawParagraph(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let ctx = cx.argument::<crate::context::BoxedContext2D>(0)?;
    let para = cx.argument::<BoxedParagraph>(1)?;
    let x = float_arg_or_bail(&mut cx, 2, "x")?;
    let y = float_arg_or_bail(&mut cx, 3, "y")?;

    let para = para.borrow_mut();
    let ctx = ctx.borrow();

    ctx.with_canvas(|canvas| {
        para.paragraph.paint(canvas, Point::new(x, y));
    });

    Ok(cx.undefined())
}