Skip to content

Commit 177f908

Browse files
authored
Merge pull request #2066 from Shopify/para-width
Compute paragraph bounding box
2 parents d61e7a8 + be80970 commit 177f908

File tree

9 files changed

+154
-14
lines changed

9 files changed

+154
-14
lines changed

docs/docs/text/paragraph.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,32 +78,42 @@ const textStyle = {
7878
};
7979
```
8080

81-
## Paragraph Height
81+
## Paragraph Bounding Box
8282

83-
To get the paragraph height, you can calculate the layout using `layout()` and once done, you can invoke `getHeight()`.
83+
Before getting the paragraph height and width, you need to compute its layout using `layout()` and and once done, you can invoke `getHeight()` for the height and `getLongestLine()` for the width.
8484

8585
```tsx twoslash
8686
import { useMemo } from "react";
87-
import { Paragraph, Skia, useFonts } from "@shopify/react-native-skia";
87+
import { Paragraph, Skia, useFonts, Canvas, Rect } from "@shopify/react-native-skia";
8888

8989
const MyParagraph = () => {
9090
const paragraph = useMemo(() => {
9191
const para = Skia.ParagraphBuilder.Make()
92-
.addText("Say Hello to ")
93-
.addText("Skia 🎨")
94-
.pop()
92+
.addText("Say Hello to React Native Skia")
9593
.build();
9694
// Calculate the layout
97-
para.layout(300);
95+
para.layout(200);
9896
return para;
9997
}, []);
10098
// Now the paragraph height is available
10199
const height = paragraph.getHeight();
100+
const width = paragraph.getLongestLine();
102101
// Render the paragraph
103-
return <Paragraph paragraph={paragraph} x={0} y={0} width={300} />;
102+
return (
103+
<Canvas style={{ width: 256, height: 256 }}>
104+
{/* Maximum paragraph width */}
105+
<Rect x={0} y={0} width={200} height={256} color="magenta" />
106+
{/* Paragraph bounding box */}
107+
<Rect x={0} y={0} width={width} height={height} color="cyan" />
108+
<Paragraph paragraph={paragraph} x={0} y={0} width={200} />
109+
</Canvas>
110+
);
104111
};
105112
```
106113

114+
<img src={require("/static/img/paragraph/boundingbox-node.png").default} width="256" height="256" />
115+
116+
107117
## Fonts
108118

109119
By default, the paragraph API will use the system fonts.
25.2 KB
Loading

package/cpp/api/JsiSkParagraph.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ class JsiSkParagraph : public JsiSkHostObject {
5555
return static_cast<double>(_paragraph->getMaxWidth());
5656
}
5757

58+
JSI_HOST_FUNCTION(getMaxIntrinsicWidth) {
59+
return static_cast<double>(_paragraph->getMaxIntrinsicWidth());
60+
}
61+
62+
JSI_HOST_FUNCTION(getMinIntrinsicWidth) {
63+
return static_cast<double>(_paragraph->getMinIntrinsicWidth());
64+
}
65+
66+
JSI_HOST_FUNCTION(getLongestLine) {
67+
return static_cast<double>(_paragraph->getLongestLine());
68+
}
69+
5870
JSI_HOST_FUNCTION(getGlyphPositionAtCoordinate) {
5971
auto dx = getArgumentAsNumber(runtime, arguments, count, 0);
6072
auto dy = getArgumentAsNumber(runtime, arguments, count, 1);
@@ -113,6 +125,9 @@ class JsiSkParagraph : public JsiSkHostObject {
113125
JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiSkParagraph, layout),
114126
JSI_EXPORT_FUNC(JsiSkParagraph, paint),
115127
JSI_EXPORT_FUNC(JsiSkParagraph, getMaxWidth),
128+
JSI_EXPORT_FUNC(JsiSkParagraph, getMinIntrinsicWidth),
129+
JSI_EXPORT_FUNC(JsiSkParagraph, getMaxIntrinsicWidth),
130+
JSI_EXPORT_FUNC(JsiSkParagraph, getLongestLine),
116131
JSI_EXPORT_FUNC(JsiSkParagraph, getHeight),
117132
JSI_EXPORT_FUNC(JsiSkParagraph, getRectsForPlaceholders),
118133
JSI_EXPORT_FUNC(JsiSkParagraph,
26.8 KB
Loading
24 KB
Loading

package/src/renderer/__tests__/e2e/Paragraphs.spec.tsx

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ describe("Paragraphs", () => {
453453
);
454454
});
455455

456-
itRunsE2eOnly("should draw the bounding box", async () => {
456+
itRunsE2eOnly("should draw the bounding box (1)", async () => {
457457
const img = await surface.drawOffscreen(
458458
(Skia, canvas, { bold, boldItalic, italic }) => {
459459
const para = Skia.ParagraphBuilder.Make()
@@ -474,10 +474,12 @@ describe("Paragraphs", () => {
474474
.pop()
475475
.build();
476476
para.layout(150);
477-
const height = para.getHeight();
478477
const paint = Skia.Paint();
479478
paint.setColor(Skia.Color("cyan"));
480-
canvas.drawRect(Skia.XYWHRect(0, 0, 150, height), paint);
479+
canvas.drawRect(
480+
Skia.XYWHRect(0, 0, para.getMaxWidth(), para.getHeight()),
481+
paint
482+
);
481483
para.paint(canvas, 0, 0);
482484
},
483485
{
@@ -492,6 +494,77 @@ describe("Paragraphs", () => {
492494
);
493495
});
494496

497+
it("should draw the bounding box (2)", async () => {
498+
const img = await surface.drawOffscreen(
499+
(Skia, canvas, ctx) => {
500+
const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
501+
Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
502+
)!;
503+
const provider = Skia.TypefaceFontProvider.Make();
504+
provider.registerFont(robotoRegular, "Roboto");
505+
const para = Skia.ParagraphBuilder.Make({}, provider)
506+
.pushStyle({
507+
fontFamilies: ["Roboto"],
508+
color: Skia.Color("black"),
509+
fontSize: 30,
510+
})
511+
.addText("Say Hello to React Native Skia")
512+
.pop()
513+
.build();
514+
para.layout(150);
515+
const paint = Skia.Paint();
516+
paint.setColor(Skia.Color("cyan"));
517+
const height = para.getHeight();
518+
const width = para.getLongestLine();
519+
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), paint);
520+
para.paint(canvas, 0, 0);
521+
},
522+
{
523+
RobotoRegular,
524+
}
525+
);
526+
checkImage(
527+
img,
528+
`snapshots/paragraph/paragraph-bounding-box-${surface.OS}.png`
529+
);
530+
});
531+
it("should draw the bounding box (3)", async () => {
532+
const img = await surface.drawOffscreen(
533+
(Skia, canvas, ctx) => {
534+
const robotoRegular = Skia.Typeface.MakeFreeTypeFaceFromData(
535+
Skia.Data.fromBytes(new Uint8Array(ctx.RobotoRegular))
536+
)!;
537+
const provider = Skia.TypefaceFontProvider.Make();
538+
provider.registerFont(robotoRegular, "Roboto");
539+
const para = Skia.ParagraphBuilder.Make({}, provider)
540+
.pushStyle({
541+
fontFamilies: ["Roboto"],
542+
color: Skia.Color("black"),
543+
fontSize: 30,
544+
})
545+
.addText("Say Hello to React Native Skia")
546+
.pop()
547+
.build();
548+
const maxWidth = ctx.canvasWidth * 0.78125;
549+
para.layout(maxWidth);
550+
const paint = Skia.Paint();
551+
paint.setColor(Skia.Color("magenta"));
552+
const height = para.getHeight();
553+
const width = para.getLongestLine();
554+
canvas.drawRect(Skia.XYWHRect(0, 0, maxWidth, ctx.canvasHeight), paint);
555+
paint.setColor(Skia.Color("cyan"));
556+
canvas.drawRect(Skia.XYWHRect(0, 0, width, height), paint);
557+
para.paint(canvas, 0, 0);
558+
},
559+
{
560+
RobotoRegular,
561+
canvasWidth: surface.width,
562+
canvasHeight: surface.height,
563+
}
564+
);
565+
checkImage(img, docPath(`paragraph/boundingbox-${surface.OS}.png`));
566+
});
567+
495568
itRunsE2eOnly("should return the paragraph height", async () => {
496569
const { width, height } = await surface.eval((Skia) => {
497570
const para = Skia.ParagraphBuilder.Make()

package/src/skia/types/Paragraph/Paragraph.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ export interface SkParagraph extends SkJSIInstance<"Paragraph"> {
3232
* method to have been called first.
3333
*/
3434
getMaxWidth(): number;
35+
/**
36+
* Returns the minimum intrinsic width of the paragraph.
37+
* The minimum intrinsic width is the width beyond which increasing the width of the paragraph
38+
* does not decrease the height. This is effectively the width at which the paragraph
39+
* can no longer wrap lines and is forced to overflow.
40+
* This method requires the layout method to have been called first.
41+
* @returns {number} The minimum intrinsic width of the paragraph.
42+
*/
43+
getMinIntrinsicWidth(): number;
44+
/**
45+
* Returns the maximum intrinsic width of the paragraph.
46+
* The maximum intrinsic width is the width at which the paragraph can layout its content without line breaks,
47+
* meaning it's the width of the widest line or the widest word if the widest line is shorter than that.
48+
* This width represents the ideal width for the paragraph to display all content in a single line without overflow.
49+
* This method requires the layout method to have been called first.
50+
* @returns {number} The maximum intrinsic width of the paragraph.
51+
*/
52+
getMaxIntrinsicWidth(): number;
53+
54+
/**
55+
* Returns the width of the longest line in the paragraph.
56+
* This method requires the layout method to have been called first.
57+
*/
58+
getLongestLine(): number;
59+
3560
/**
3661
* Returns the index of the glyph at the given position. This method requires
3762
* the layout method to have been called first.

package/src/skia/web/JsiSkParagraph.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ export class JsiSkParagraph
1212
constructor(CanvasKit: CanvasKit, ref: Paragraph) {
1313
super(CanvasKit, ref, "Paragraph");
1414
}
15+
getMinIntrinsicWidth(): number {
16+
return this.ref.getMinIntrinsicWidth();
17+
}
18+
19+
getMaxIntrinsicWidth(): number {
20+
return this.ref.getMaxIntrinsicWidth();
21+
}
22+
23+
getLongestLine(): number {
24+
return this.ref.getLongestLine();
25+
}
1526

1627
layout(width: number): void {
1728
this.ref.layout(width);

package/src/skia/web/JsiSkParagraphBuilderFactory.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
import { Host } from "./Host";
1010
import { JsiSkParagraphBuilder } from "./JsiSkParagraphBuilder";
1111
import { JsiSkParagraphStyle } from "./JsiSkParagraphStyle";
12-
import { JsiSkTypeface } from "./JsiSkTypeface";
12+
import { JsiSkTypefaceFontProvider } from "./JsiSkTypefaceFontProvider";
1313

1414
export class JsiSkParagraphBuilderFactory
1515
extends Host
@@ -31,11 +31,17 @@ export class JsiSkParagraphBuilderFactory
3131
"SkTypefaceFontProvider is required on React Native Web."
3232
);
3333
}
34+
const fontCollection = this.CanvasKit.FontCollection.Make();
35+
fontCollection.setDefaultFontManager(
36+
JsiSkTypefaceFontProvider.fromValue(typefaceProvider)
37+
);
38+
fontCollection.enableFontFallback();
39+
3440
return new JsiSkParagraphBuilder(
3541
this.CanvasKit,
36-
this.CanvasKit.ParagraphBuilder.MakeFromFontProvider(
42+
this.CanvasKit.ParagraphBuilder.MakeFromFontCollection(
3743
style,
38-
JsiSkTypeface.fromValue(typefaceProvider)
44+
fontCollection
3945
)
4046
);
4147
}

0 commit comments

Comments
 (0)