Tldraw/packages/editor/src/lib/editor/shapeutils/FrameShapeUtil/FrameShapeUtil.tsx

210 wiersze
5.9 KiB
TypeScript
Czysty Zwykły widok Historia

2023-04-25 11:01:25 +00:00
import { canolicalizeRotation, SelectionEdge, toDomPrecision } from '@tldraw/primitives'
import { TLFrameShape, TLShape, TLShapeId } from '@tldraw/tlschema'
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
import { last } from '@tldraw/utils'
2023-04-25 11:01:25 +00:00
import { SVGContainer } from '../../../components/SVGContainer'
import { defaultEmptyAs } from '../../../utils/string'
import { BaseBoxShapeUtil } from '../BaseBoxShapeUtil'
import { TLOnResizeEndHandler } from '../ShapeUtil'
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
import { createTextSvgElementFromSpans } from '../shared/createTextSvgElementFromSpans'
2023-04-25 11:01:25 +00:00
import { TLExportColors } from '../shared/TLExportColors'
import { FrameHeading } from './components/FrameHeading'
/** @public */
export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
static override type = 'frame'
2023-04-25 11:01:25 +00:00
override canBind = () => true
override canEdit = () => true
override defaultProps(): TLFrameShape['props'] {
return { w: 160 * 2, h: 90 * 2, name: '' }
2023-04-25 11:01:25 +00:00
}
override render(shape: TLFrameShape) {
const bounds = this.bounds(shape)
return (
<>
<SVGContainer>
<rect className="tl-hitarea-stroke" width={bounds.width} height={bounds.height} />
2023-04-25 11:01:25 +00:00
<rect
className="tl-frame__body"
2023-04-25 11:01:25 +00:00
width={bounds.width}
height={bounds.height}
fill="none"
/>
</SVGContainer>
<FrameHeading
id={shape.id}
name={shape.props.name}
width={bounds.width}
height={bounds.height}
/>
</>
)
}
override toSvg(
shape: TLFrameShape,
font: string,
colors: TLExportColors
): SVGElement | Promise<SVGElement> {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('width', shape.props.w.toString())
rect.setAttribute('height', shape.props.h.toString())
rect.setAttribute('fill', colors.solid)
rect.setAttribute('stroke', colors.fill.black)
rect.setAttribute('stroke-width', '1')
rect.setAttribute('rx', '1')
rect.setAttribute('ry', '1')
g.appendChild(rect)
// Text label
const pageRotation = canolicalizeRotation(this.editor.getPageRotationById(shape.id))
2023-04-25 11:01:25 +00:00
// rotate right 45 deg
const offsetRotation = pageRotation + Math.PI / 4
const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4
const labelSide: SelectionEdge = (['top', 'left', 'bottom', 'right'] as const)[
Math.floor(scaledRotation)
]
let labelTranslate: string
switch (labelSide) {
case 'top':
labelTranslate = ``
break
case 'right':
labelTranslate = `translate(${toDomPrecision(shape.props.w)}px, 0px) rotate(90deg)`
break
case 'bottom':
labelTranslate = `translate(${toDomPrecision(shape.props.w)}px, ${toDomPrecision(
shape.props.h
)}px) rotate(180deg)`
break
case 'left':
labelTranslate = `translate(0px, ${toDomPrecision(shape.props.h)}px) rotate(270deg)`
break
default:
labelTranslate = ``
}
// Truncate with ellipsis
const opts = {
fontSize: 12,
fontFamily: 'Inter, sans-serif',
textAlign: 'start' as const,
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
width: shape.props.w,
height: 32,
padding: 0,
2023-04-25 11:01:25 +00:00
lineHeight: 1,
fontStyle: 'normal',
fontWeight: 'normal',
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
overflow: 'truncate-ellipsis' as const,
verticalTextAlign: 'middle' as const,
2023-04-25 11:01:25 +00:00
}
const spans = this.editor.textMeasure.measureTextSpans(
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
defaultEmptyAs(shape.props.name, 'Frame') + String.fromCharCode(8203),
opts
)
2023-04-25 11:01:25 +00:00
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
const firstSpan = spans[0]
const lastSpan = last(spans)!
const labelTextWidth = lastSpan.box.w + lastSpan.box.x - firstSpan.box.x
const text = createTextSvgElementFromSpans(this.editor, spans, {
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
offsetY: -opts.height - 2,
2023-04-25 11:01:25 +00:00
...opts,
})
text.style.setProperty('transform', labelTranslate)
const textBg = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
Measure individual words instead of just line breaks for text exports (#1397) This diff fixes a number of issues with text export by completely overhauling how we approach laying out text in exports. Currently, we try to carefully replicate in-browser behaviour around line breaks and whitespace collapsing. We do this using an iterative algorithm that forces the browser to perform a layout for each word, and attempting to re-implement how the browser does things like whitespace collapsing & finding line break opportunities. Lots of export issues come from the fact that this is almost impossible to do well (short of sending a complete text layout algorithm & full unicode lookup tables). Luckily, the browser already has a complete text layout algorithm and full unicode lookup tables! In the new approach, we ask the browser to lay the text out once. Then, we use the [`Range`](https://developer.mozilla.org/en-US/docs/Web/API/Range) API to loop over every character in the rendered text and measure its position. These character positions are then grouped into "spans". A span is a contiguous range of either whitespace or non-whitespace characters, uninterrupted by any browser-inserting line breaks. When we come to render the SVG, each span gets its own `<tspan>` element, absolutely positioned according to where it ended up in the user's browser. This fixes a bunch of issues: **Misaligned text due to whitespace collapsing at line breaks** ![Kapture 2023-05-17 at 12 07 30](https://github.com/tldraw/tldraw/assets/1489520/5ab66fe0-6ceb-45bb-8787-90ccb124664a) **Hyphenated text (or text with non-trivial/whitespace-based breaking rules like Thai) not splitting correctly** ![Kapture 2023-05-17 at 12 21 40](https://github.com/tldraw/tldraw/assets/1489520/d2d5fd13-3e79-48c4-8e76-ae2c70a6471e) **Weird alignment issues in note shapes** ![Kapture 2023-05-17 at 12 24 59](https://github.com/tldraw/tldraw/assets/1489520/a0e51d57-7c1c-490e-9952-b92417ffdf9e) **Frame labels not respecting multiple spaces & not truncating correctly** ![Kapture 2023-05-17 at 12 27 27](https://github.com/tldraw/tldraw/assets/1489520/39b2f53c-0180-460e-b10a-9fd955a6fa78) #### Quick note on browser compatibility This approach works well across all browsers, but in some cases actually _increases_ x-browser variance. Consider these screenshots of the same element (original above, export below): ![image](https://github.com/tldraw/tldraw/assets/1489520/5633b041-8cb3-4c92-bef6-4f3c202305de) Notice how on chrome, the whitespace at the end of each line of right-aligned text is preserved. On safari, it's collapsed. The safari option looks better - so our manual line-breaking/white-space-collapsing algorithm preferred safari's approach. That meant that in-app, this shape looks very slightly different from browser to browser. But out of the app, the exports would have been the same (although also note that hyphenation is broken). Now, because these shapes look different across browsers, the exports now look different across browsers too. We're relying on the host-browsers text layout algorithm, which means we'll faithfully reproduce any quirks/inconsistencies of that algorithm. I think this is an acceptable tradeoff. ### Change Type - [x] `patch` — Bug Fix ### Test Plan * Comprehensive testing of text in exports, paying close attention to details around white-space, line-breaking and alignment * Consider setting `tldrawDebugSvg = true` * Check text shapes, geo shapes with labels, arrow shapes with labels, note shapes, frame labels * Check different alignments and fonts (including vertical alignment) ### Release Notes - Add a brief release note for your PR here.
2023-05-22 15:10:03 +00:00
textBg.setAttribute('x', '-8px')
textBg.setAttribute('y', -opts.height - 4 + 'px')
textBg.setAttribute('width', labelTextWidth + 16 + 'px')
textBg.setAttribute('height', `${opts.height}px`)
2023-04-25 11:01:25 +00:00
textBg.setAttribute('rx', 4 + 'px')
textBg.setAttribute('ry', 4 + 'px')
textBg.setAttribute('fill', colors.background)
g.appendChild(textBg)
g.appendChild(text)
return g
}
indicator(shape: TLFrameShape) {
const bounds = this.bounds(shape)
return (
<rect
width={toDomPrecision(bounds.width)}
height={toDomPrecision(bounds.height)}
className={`tl-frame-indicator`}
2023-04-25 11:01:25 +00:00
/>
)
}
override canReceiveNewChildrenOfType = (shape: TLShape, _type: TLShape['type']) => {
return !shape.isLocked
}
override providesBackgroundForChildren(): boolean {
2023-04-25 11:01:25 +00:00
return true
}
override canDropShapes = (shape: TLFrameShape, _shapes: TLShape[]): boolean => {
return !shape.isLocked
2023-04-25 11:01:25 +00:00
}
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
if (!shapes.every((child) => child.parentId === frame.id)) {
this.editor.reparentShapesById(
2023-04-25 11:01:25 +00:00
shapes.map((shape) => shape.id),
frame.id
)
return { shouldHint: true }
}
return { shouldHint: false }
}
onDragShapesOut = (_shape: TLFrameShape, shapes: TLShape[]): void => {
const parentId = this.editor.getShapeById(_shape.parentId)
2023-04-25 11:01:25 +00:00
const isInGroup = parentId?.type === 'group'
// If frame is in a group, keep the shape
// moved out in that group
if (isInGroup) {
this.editor.reparentShapesById(
2023-04-25 11:01:25 +00:00
shapes.map((shape) => shape.id),
parentId.id
)
} else {
this.editor.reparentShapesById(
2023-04-25 11:01:25 +00:00
shapes.map((shape) => shape.id),
this.editor.currentPageId
2023-04-25 11:01:25 +00:00
)
}
}
override onResizeEnd: TLOnResizeEndHandler<TLFrameShape> = (shape) => {
const bounds = this.editor.getPageBounds(shape)!
const children = this.editor.getSortedChildIds(shape.id)
2023-04-25 11:01:25 +00:00
const shapesToReparent: TLShapeId[] = []
for (const childId of children) {
const childBounds = this.editor.getPageBoundsById(childId)!
2023-04-25 11:01:25 +00:00
if (!bounds.includes(childBounds)) {
shapesToReparent.push(childId)
}
}
if (shapesToReparent.length > 0) {
this.editor.reparentShapesById(shapesToReparent, this.editor.currentPageId)
2023-04-25 11:01:25 +00:00
}
}
}