[improvement] labels (#467)

* Improve appearance of arrow shape labels, set label text color to match stroke

* fix svg copy fonts for text, labels

* Reduce opacity effect of scaled arrow label, update font in export, adds command+enter to finish text
This commit is contained in:
Steve Ruiz 2021-12-28 11:23:17 +00:00 committed by GitHub
parent f57d6bda94
commit d0fd712e5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 253 additions and 156 deletions

View file

@ -2,7 +2,7 @@
import * as React from 'react'
import Utils from '../utils'
import type { TLBounds, TLComponentProps, TLForwardedRef, TLShape, TLUser } from '../types'
import { intersectPolygonBounds, intersectPolylineBounds } from '@tldraw/intersect'
import { intersectPolygonBounds } from '@tldraw/intersect'
export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M = any> {
refMap = new Map<string, React.RefObject<E>>()
@ -21,6 +21,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M
shape: T
meta: M
user?: TLUser<T>
bounds: TLBounds
isHovered: boolean
isSelected: boolean
}) => React.ReactElement | null
@ -64,6 +65,7 @@ export abstract class TLShapeUtil<T extends TLShape, E extends Element = any, M
meta: M
isHovered: boolean
isSelected: boolean
bounds: TLBounds
}) => JSX.Element
) => component
}

View file

@ -10,7 +10,7 @@ export const HTMLContainer = React.forwardRef<HTMLDivElement, HTMLContainerProps
return (
<Observer>
{() => (
<div ref={ref} className={`tl-positioned-div ${className}`} {...rest}>
<div ref={ref} className={`tl-positioned-div ${className}`} draggable={false} {...rest}>
<div className="tl-inner-div">{children}</div>
</div>
)}

View file

@ -28,6 +28,7 @@ export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape
return (
<div
ref={rPositioned}
draggable={false}
className={[
'tl-indicator',
'tl-absolute',
@ -41,6 +42,7 @@ export const ShapeIndicator = observer(function ShapeIndicator<T extends TLShape
shape={shape}
meta={meta}
user={user}
bounds={bounds}
isSelected={isSelected}
isHovered={isHovered}
/>

View file

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const LETTER_SPACING = '-0.03em'
export const LINE_HEIGHT = 1.3
export const GRID_SIZE = 8
export const SVG_EXPORT_PADDING = 16
export const BINDING_DISTANCE = 16

View file

@ -4,7 +4,7 @@ const styles = new Map<string, HTMLStyleElement>()
const UID = `Tldraw-fonts`
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro');
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');
`
export function useStylesheet() {

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { mockDocument, TldrawTestApp } from '~test'
import { ArrowShape, ColorStyle, RectangleShape, SessionType, TDShapeType } from '~types'
import { ArrowShape, ColorStyle, SessionType, TDShapeType } from '~types'
import type { SelectTool } from './tools/SelectTool'
describe('TldrawTestApp', () => {

View file

@ -15,7 +15,6 @@ import {
Utils,
TLBounds,
TLDropEventHandler,
TLPerformanceMode,
} from '@tldraw/core'
import {
FlipType,
@ -1821,7 +1820,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// Embed our custom fonts
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');`
style.textContent = `@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Crimson+Pro&display=block');`
defs.appendChild(style)
svg.appendChild(defs)
// Get the shapes in order

View file

@ -227,8 +227,8 @@ Array [
]
`;
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs><g/></svg>"`;
exports[`TldrawTestApp When copying to SVG Copies grouped shapes.: copied svg with group 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Crimson+Pro&amp;display=block');</style></defs><g/></svg>"`;
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs></svg>"`;
exports[`TldrawTestApp When copying to SVG Copies shapes.: copied svg 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 236.74 236.74\\" width=\\"204.74\\" height=\\"204.74\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Crimson+Pro&amp;display=block');</style></defs></svg>"`;
exports[`TldrawTestApp When copying to SVG Respects child index: copied svg with reordered elements 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Source+Serif+Pro&amp;display=swap');</style></defs></svg>"`;
exports[`TldrawTestApp When copying to SVG Respects child index: copied svg with reordered elements 1`] = `"<svg xmlns=\\"http://www.w3.org/2000/svg\\" viewBox=\\"0 0 232 232\\" width=\\"200\\" height=\\"200\\" fill=\\"transparent\\"><defs><style>@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&amp;family=Source+Code+Pro&amp;family=Source+Sans+Pro&amp;family=Crimson+Pro&amp;display=block');</style></defs></svg>"`;

View file

@ -33,10 +33,11 @@ import {
} from './arrowHelpers'
import { getTrianglePoints } from '../TriangleUtil/triangleHelpers'
import { styled } from '~styles'
import { TextLabel, getFontStyle } from '../shared'
import { TextLabel, getFontStyle, getShapeStyle } from '../shared'
import { getTextLabelSize } from '../shared/getTextSize'
import { StraightArrow } from './components/StraightArrow'
import { CurvedArrow } from './components/CurvedArrow.tsx'
import { LabelMask } from '../shared/LabelMask'
type T = ArrowShape
type E = HTMLDivElement
@ -107,6 +108,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
} = shape
const isStraightLine = Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
const font = getFontStyle(style)
const styles = getShapeStyle(style)
const labelSize = label || isEditing ? getTextLabelSize(label, font) : [0, 0]
const bounds = this.getBounds(shape)
const dist = React.useMemo(() => {
@ -137,15 +139,15 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
return (
<FullWrapper ref={ref} {...events}>
<TextLabel
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
isDarkMode={meta.isDarkMode}
font={font}
text={label}
color={styles.stroke}
offsetX={offset[0]}
offsetY={offset[1]}
scale={scale}
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
/>
<SVGContainer id={shape.id + '_svg'}>
<defs>
@ -165,7 +167,7 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
rx={4 * scale}
ry={4 * scale}
fill="black"
opacity={Math.max(scale, 0.8)}
opacity={Math.max(scale, 0.9)}
/>
</mask>
</defs>
@ -193,23 +195,66 @@ export class ArrowUtil extends TDShapeUtil<T, E> {
}
)
Indicator = TDShapeUtil.Indicator<ArrowShape>(({ shape }) => {
Indicator = TDShapeUtil.Indicator<ArrowShape>(({ shape, bounds }) => {
const {
style,
decorations,
label,
handles: { start, bend, end },
} = shape
const font = getFontStyle(style)
const labelSize = label ? getTextLabelSize(label, font) : [0, 0]
const isStraightLine = Vec.dist(bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1
const dist = React.useMemo(() => {
const { start, bend, end } = shape.handles
if (isStraightLine) return Vec.dist(start.point, end.point)
const circle = getCtp(start.point, bend.point, end.point)
const center = circle.slice(0, 2)
const radius = circle[2]
const length = getArcLength(center, radius, start.point, end.point)
return Math.abs(length)
}, [shape.handles])
const scale = Math.max(
0.5,
Math.min(1, Math.max(dist / (labelSize[1] + 128), dist / (labelSize[0] + 128)))
)
const offset = React.useMemo(() => {
const bounds = this.getBounds(shape)
const offset = Vec.sub(shape.handles.bend.point, [bounds.width / 2, bounds.height / 2])
return offset
}, [shape, scale])
return (
<path
d={getArrowPath(
style,
start.point,
bend.point,
end.point,
decorations?.start,
decorations?.end
<>
<LabelMask
id={shape.id}
scale={scale}
offset={offset}
bounds={bounds}
labelSize={labelSize}
/>
<path
d={getArrowPath(
style,
start.point,
bend.point,
end.point,
decorations?.start,
decorations?.end
)}
mask={label ? `url(#${shape.id}_clip)` : ``}
/>
{label && (
<rect
x={bounds.width / 2 - (labelSize[0] / 2) * scale + offset[0]}
y={bounds.height / 2 - (labelSize[1] / 2) * scale + offset[1]}
width={labelSize[0] * scale}
height={labelSize[1] * scale}
rx={4 * scale}
ry={4 * scale}
fill="transparent"
/>
)}
/>
</>
)
})

View file

@ -82,9 +82,9 @@ export class EllipseUtil extends TDShapeUtil<T, E> {
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
isDarkMode={meta.isDarkMode}
font={font}
text={label}
color={styles.stroke}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={(labelPoint[1] - 0.5) * bounds.height}
/>

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { Utils, HTMLContainer } from '@tldraw/core'
import { TDShapeType, TDMeta, ImageShape, TDAsset, TDImageAsset } from '~types'
import { TDShapeType, TDMeta, ImageShape, TDImageAsset } from '~types'
import { GHOSTED_OPACITY } from '~constants'
import { TDShapeUtil } from '../TDShapeUtil'
import {
@ -193,5 +193,6 @@ const ImageElement = styled('img', {
minWidth: '100%',
pointerEvents: 'none',
objectFit: 'cover',
userSelect: 'none',
borderRadius: 2,
})

View file

@ -79,9 +79,9 @@ export class RectangleUtil extends TDShapeUtil<T, E> {
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
isDarkMode={meta.isDarkMode}
font={font}
text={label}
color={styles.stroke}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={(labelPoint[1] - 0.5) * bounds.height}
/>

View file

@ -275,15 +275,19 @@ export class StickyUtil extends TDShapeUtil<T, E> {
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const textElm = getTextSvgElement(shape, bounds)
const textBounds = Utils.expandBounds(bounds, -PADDING)
const textElm = getTextSvgElement(shape.text, shape.style, textBounds)
const style = getStickyShapeStyle(shape.style)
textElm.setAttribute('fill', style.color)
textElm.setAttribute('transform', `translate(${PADDING}, ${PADDING})`)
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', bounds.width + '')
rect.setAttribute('height', bounds.height + '')
rect.setAttribute('fill', style.fill)
rect.setAttribute('rx', '3')
rect.setAttribute('ry', '3')
g.appendChild(rect)
g.appendChild(textElm)

View file

@ -11,6 +11,9 @@ import { Vec } from '@tldraw/vec'
import type { TDBinding, TDMeta, TDShape, TransformInfo } from '~types'
import * as React from 'react'
import { BINDING_DISTANCE } from '~constants'
import { getTextSvgElement } from './shared/getTextSvgElement'
import { getTextLabelSize } from './shared/getTextSize'
import { getFontStyle, getShapeStyle } from './shared'
export abstract class TDShapeUtil<T extends TDShape, E extends Element = any> extends TLShapeUtil<
T,
@ -188,6 +191,24 @@ export abstract class TDShapeUtil<T extends TDShape, E extends Element = any> ex
onSessionComplete?: (shape: T) => Partial<T> | void
getSvgElement = (shape: T): SVGElement | void => {
return document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
const elm = document.getElementById(shape.id + '_svg')?.cloneNode(true) as SVGElement
if (!elm) return // possibly in test mode
if ('label' in shape && shape.label !== undefined) {
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const bounds = this.getBounds(shape)
const labelElm = getTextSvgElement(shape.label, shape.style, bounds)
labelElm.setAttribute('fill', getShapeStyle(shape.style).stroke)
const font = getFontStyle(shape.style)
const size = getTextLabelSize(shape.label, font)
labelElm.setAttribute('transform-origin', 'top left')
labelElm.setAttribute(
'transform',
`translate(${(bounds.width - size[0]) / 2}, ${(bounds.height - size[1]) / 2})`
)
g.appendChild(elm)
g.appendChild(labelElm)
return g
}
return elm
}
}

View file

@ -12,6 +12,7 @@ import { TLDR } from '~state/TLDR'
import { getTextAlign } from '../shared/getTextAlign'
import { getTextSvgElement } from '../shared/getTextSvgElement'
import { stopPropagation } from '~components/stopPropagation'
import { useTextKeyboardEvents } from '../shared/useTextKeyboardEvents'
type T = TextShape
type E = HTMLDivElement
@ -86,57 +87,23 @@ export class TextUtil extends TDShapeUtil<T, E> {
}
onShapeChange?.({
...shape,
id: shape.id,
point: Vec.sub(shape.point, delta),
text: TLDR.normalizeText(e.currentTarget.value),
})
},
[shape]
[shape.id, shape.point]
)
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// If this keydown was just the meta key or a shortcut
// that includes holding the meta key like (Command+V)
// then leave the event untouched. We also have to explicitly
// Implement undo/redo for some reason in order to get this working
// in the vscode extension. Without the below code the following doesn't work
//
// - You can't cut/copy/paste when when text-editing/focused
// - You can't undo/redo when when text-editing/focused
// - You can't use Command+A to select all the text, when when text-editing/focused
if (!(e.key === 'Meta' || e.metaKey)) {
e.stopPropagation()
} else if (e.key === 'z' && e.metaKey) {
if (e.shiftKey) {
document.execCommand('redo', false)
} else {
document.execCommand('undo', false)
}
e.stopPropagation()
e.preventDefault()
return
}
if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
e.currentTarget.blur()
return
}
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
}
const onChange = React.useCallback(
(text: string) => {
onShapeChange?.({ id: shape.id, text })
},
[shape, onShapeChange]
[shape.id]
)
const handleKeyDown = useTextKeyboardEvents(onChange)
const handleBlur = React.useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
e.currentTarget.setSelectionRange(0, 0)
onShapeBlur?.()
@ -341,7 +308,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
getSvgElement = (shape: T): SVGElement | void => {
const bounds = this.getBounds(shape)
const elm = getTextSvgElement(shape, bounds)
const elm = getTextSvgElement(shape.text, shape.style, bounds)
elm.setAttribute('fill', getShapeStyle(shape.style).stroke)
return elm
}

View file

@ -20,7 +20,7 @@ import { getTriangleCentroid, getTrianglePoints } from './triangleHelpers'
import { styled } from '~styles'
import { DrawTriangle } from './components/DrawTriangle'
import { DashedTriangle } from './components/DashedTriangle'
import { TextLabel } from '../shared/TextLabel'
import { TextLabel, getShapeStyle } from '../shared'
import { TriangleBindingIndicator } from './components/TriangleBindingIndicator'
type T = TriangleShape
@ -72,6 +72,7 @@ export class TriangleUtil extends TDShapeUtil<T, E> {
) => {
const { id, label = '', size, style, labelPoint = LABEL_POINT } = shape
const font = getFontStyle(style)
const styles = getShapeStyle(style)
const Component = style.dash === DashStyle.Draw ? DrawTriangle : DashedTriangle
const handleLabelChange = React.useCallback(
(label: string) => onShapeChange?.({ id, label }),
@ -85,14 +86,14 @@ export class TriangleUtil extends TDShapeUtil<T, E> {
return (
<FullWrapper ref={ref} {...events}>
<TextLabel
font={font}
text={label}
color={styles.stroke}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={offsetY + (labelPoint[1] - 0.5) * bounds.height}
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onShapeBlur}
isDarkMode={meta.isDarkMode}
font={font}
text={label}
offsetX={(labelPoint[0] - 0.5) * bounds.width}
offsetY={offsetY + (labelPoint[1] - 0.5) * bounds.height}
/>
<SVGContainer id={shape.id + '_svg'} opacity={isGhost ? GHOSTED_OPACITY : 1}>
{isBinding && <TriangleBindingIndicator size={size} />}

View file

@ -237,5 +237,6 @@ const VideoElement = styled('video', {
minWidth: '100%',
pointerEvents: 'none',
objectFit: 'cover',
userSelect: 'none',
borderRadius: 2,
})

View file

@ -0,0 +1,36 @@
import type { TLBounds } from '@tldraw/core'
import * as React from 'react'
interface WithLabelMaskProps {
id: string
bounds: TLBounds
labelSize: number[]
offset?: number[]
scale?: number
}
export function LabelMask({ id, bounds, labelSize, offset, scale = 1 }: WithLabelMaskProps) {
return (
<defs>
<mask id={id + '_clip'}>
<rect
x={-100}
y={-100}
width={bounds.width + 200}
height={bounds.height + 200}
fill="white"
/>
<rect
x={bounds.width / 2 - (labelSize[0] / 2) * scale + (offset?.[0] || 0)}
y={bounds.height / 2 - (labelSize[1] / 2) * scale + (offset?.[1] || 0)}
width={labelSize[0] * scale}
height={labelSize[1] * scale}
rx={4 * scale}
ry={4 * scale}
fill="black"
opacity={Math.max(scale, 0.8)}
/>
</mask>
</defs>
)
}

View file

@ -1,15 +1,16 @@
import * as React from 'react'
import { stopPropagation } from '~components/stopPropagation'
import { GHOSTED_OPACITY, LABEL_POINT, LETTER_SPACING } from '~constants'
import { GHOSTED_OPACITY, LETTER_SPACING } from '~constants'
import { TLDR } from '~state/TLDR'
import { styled } from '~styles'
import { TextAreaUtils } from '.'
import { getTextLabelSize } from './getTextSize'
import { useTextKeyboardEvents } from './useTextKeyboardEvents'
export interface TextLabelProps {
font: string
text: string
isDarkMode: boolean
color: string
onBlur?: () => void
onChange: (text: string) => void
offsetY?: number
@ -19,9 +20,9 @@ export interface TextLabelProps {
}
export const TextLabel = React.memo(function TextLabel({
isDarkMode,
font,
text,
color,
offsetX = 0,
offsetY = 0,
scale = 1,
@ -32,7 +33,6 @@ export const TextLabel = React.memo(function TextLabel({
const rInput = React.useRef<HTMLTextAreaElement>(null)
const rIsMounted = React.useRef(false)
const size = getTextLabelSize(text, font)
const color = isDarkMode ? 'white' : 'black'
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
@ -41,48 +41,7 @@ export const TextLabel = React.memo(function TextLabel({
[onChange]
)
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// If this keydown was just the meta key or a shortcut
// that includes holding the meta key like (Command+V)
// then leave the event untouched. We also have to explicitly
// Implement undo/redo for some reason in order to get this working
// in the vscode extension. Without the below code the following doesn't work
//
// - You can't cut/copy/paste when when text-editing/focused
// - You can't undo/redo when when text-editing/focused
// - You can't use Command+A to select all the text, when when text-editing/focused
if (!(e.key === 'Meta' || e.metaKey)) {
e.stopPropagation()
} else if (e.key === 'z' && e.metaKey) {
if (e.shiftKey) {
document.execCommand('redo', false)
} else {
document.execCommand('undo', false)
}
e.stopPropagation()
e.preventDefault()
return
}
if (e.key === 'Escape' || (e.key === 'Enter' && (e.ctrlKey || e.metaKey))) {
e.currentTarget.blur()
return
}
if (e.key === 'Tab') {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
onChange(TLDR.normalizeText(e.currentTarget.value))
}
},
[onChange]
)
const handleKeyDown = useTextKeyboardEvents(onChange)
const handleBlur = React.useCallback(
(e: React.FocusEvent<HTMLTextAreaElement>) => {

View file

@ -1,43 +1,39 @@
import type { TLBounds } from '@tldraw/core'
import { AlignStyle, StickyShape, TextShape } from '~types'
import { AlignStyle, ShapeStyles } from '~types'
import { getFontFace, getFontSize } from './shape-styles'
import { getTextAlign } from './getTextAlign'
import { LINE_HEIGHT } from '~constants'
export function getTextSvgElement(shape: TextShape | StickyShape, bounds: TLBounds) {
const { text, style } = shape
const fontSize = getFontSize(shape.style.size, shape.style.font)
export function getTextSvgElement(text: string, style: ShapeStyles, bounds: TLBounds) {
const fontSize = getFontSize(style.size, style.font)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const LINE_HEIGHT = fontSize * 1.3
const textLines = text.split('\n').map((line, i) => {
const textElm = document.createElementNS('http://www.w3.org/2000/svg', 'text')
textElm.textContent = line
textElm.setAttribute('font-family', getFontFace(style.font))
textElm.setAttribute('font-size', fontSize + 'px')
textElm.setAttribute('text-anchor', 'start')
textElm.setAttribute('alignment-baseline', 'central')
textElm.setAttribute('text-align', getTextAlign(style.textAlign))
textElm.setAttribute('y', LINE_HEIGHT * (0.5 + i) + '')
textElm.setAttribute('y', LINE_HEIGHT * fontSize * (0.5 + i) + '')
g.appendChild(textElm)
return textElm
})
if (style.textAlign === AlignStyle.Middle) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width / 2 + '')
textElm.setAttribute('text-align', 'center')
textElm.setAttribute('text-anchor', 'middle')
})
} else if (style.textAlign === AlignStyle.End) {
textLines.forEach((textElm) => {
textElm.setAttribute('x', bounds.width + '')
textElm.setAttribute('text-align', 'right')
textElm.setAttribute('text-anchor', 'end')
})
g.setAttribute('font-size', fontSize + '')
g.setAttribute('font-family', getFontFace(style.font).slice(1, -1))
g.setAttribute('text-align', getTextAlign(style.textAlign))
switch (style.textAlign) {
case AlignStyle.Middle: {
g.setAttribute('text-align', 'center')
g.setAttribute('text-anchor', 'middle')
textLines.forEach((textElm) => textElm.setAttribute('x', bounds.width / 2 + ''))
break
}
case AlignStyle.End: {
g.setAttribute('text-align', 'right')
g.setAttribute('text-anchor', 'end')
textLines.forEach((textElm) => textElm.setAttribute('x', bounds.width + ''))
break
}
case AlignStyle.Start: {
g.setAttribute('text-anchor', 'start')
g.setAttribute('alignment-baseline', 'central')
}
}
return g
}

View file

@ -63,7 +63,7 @@ export const fills: Record<Theme, Record<ColorStyle, string>> = {
},
dark: {
...(Object.fromEntries(
Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasDark, 0.618)])
Object.entries(colors).map(([k, v]) => [k, Utils.lerpColor(v, canvasDark, 0.82)])
) as Record<ColorStyle, string>),
[ColorStyle.White]: 'rgb(30,33,37)',
[ColorStyle.Black]: '#1e1e1f',
@ -86,7 +86,7 @@ const fontSizes = {
const fontFaces = {
[FontStyle.Script]: '"Caveat Brush"',
[FontStyle.Sans]: '"Source Sans Pro", sans-serif',
[FontStyle.Serif]: '"Source Serif Pro", serif',
[FontStyle.Serif]: '"Crimson Pro", serif',
[FontStyle.Mono]: '"Source Code Pro", monospace',
}

View file

@ -0,0 +1,62 @@
import * as React from 'react'
import { TLDR } from '~state/TLDR'
import { TextAreaUtils } from '.'
export function useTextKeyboardEvents(onChange: (text: string) => void) {
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// If this keydown was just the meta key or a shortcut
// that includes holding the meta key like (Command+V)
// then leave the event untouched. We also have to explicitly
// Implement undo/redo for some reason in order to get this working
// in the vscode extension. Without the below code the following doesn't work
//
// - You can't cut/copy/paste when when text-editing/focused
// - You can't undo/redo when when text-editing/focused
// - You can't use Command+A to select all the text, when when text-editing/focused
if (e.metaKey) e.stopPropagation()
switch (e.key) {
case 'Meta': {
e.stopPropagation()
break
}
case 'z': {
if (e.metaKey) {
if (e.shiftKey) {
document.execCommand('redo', false)
} else {
document.execCommand('undo', false)
}
e.preventDefault()
}
break
}
case 'Escape': {
e.currentTarget.blur()
break
}
case 'Enter': {
if (e.ctrlKey || e.metaKey) {
e.currentTarget.blur()
}
break
}
case 'Tab': {
e.preventDefault()
if (e.shiftKey) {
TextAreaUtils.unindent(e.currentTarget)
} else {
TextAreaUtils.indent(e.currentTarget)
}
onChange(TLDR.normalizeText(e.currentTarget.value))
break
}
}
},
[onChange]
)
return handleKeyDown
}