Skip to content

Commit

Permalink
Feat (Whiteboards): Support text overlay on shapes (#7473)
Browse files Browse the repository at this point in the history
* wip: add text to box shape

* chore: support font weight  and italic

* fix: editing styles of label

* fix: chevron color

* fix: label auto resize

* fix: textarea background

* fix: title class

* fix: centroid calculation

* fix: label size calculation

* chore: add label bounds indicator

* fix: text label width

* fix: label scale calculation

* fix: triangle label offset

* chore: activate edit mode on double click

* wip: add text to box shape

* chore: support font weight  and italic

* fix: editing styles of label

* fix: chevron color

* fix: label auto resize

* fix: textarea background

* fix: remove unused import

* chore: add label ellipse and polygon

* fix: centroid calculation

* fix: label size calculation

* chore: add label bounds indicator

* fix: text label width

* fix: label scale calculation

* fix: triangle label offset

* chore: activate edit mode on double click

* fix: remove placeholder element

* fix: label pointer events

* fix: label position

Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
  • Loading branch information
sprocketc and tiensonqin committed Dec 7, 2022
1 parent 842c139 commit da0f3eb
Show file tree
Hide file tree
Showing 9 changed files with 457 additions and 138 deletions.
Expand Up @@ -46,12 +46,7 @@ export const contextBarActionTypes = [
] as const

type ContextBarActionType = typeof contextBarActionTypes[number]
const singleShapeActions: ContextBarActionType[] = [
'Edit',
'YoutubeLink',
'IFrameSource',
'Links',
]
const singleShapeActions: ContextBarActionType[] = ['Edit', 'YoutubeLink', 'IFrameSource', 'Links']

const contextBarActionMapping = new Map<ContextBarActionType, React.FC>()

Expand All @@ -68,13 +63,13 @@ export const shapeMapping: Record<ShapeType, ContextBarActionType[]> = {
],
youtube: ['YoutubeLink', 'Links'],
iframe: ['IFrameSource', 'Links'],
box: ['Swatch', 'NoFill', 'StrokeType', 'Links'],
ellipse: ['Swatch', 'NoFill', 'StrokeType', 'Links'],
polygon: ['Swatch', 'NoFill', 'StrokeType', 'Links'],
line: ['Edit', 'Swatch', 'ArrowMode', 'Links'],
box: ['Edit', 'TextStyle', 'Swatch', 'NoFill', 'StrokeType', 'Links'],
ellipse: ['Edit', 'TextStyle', 'Swatch', 'NoFill', 'StrokeType', 'Links'],
polygon: ['Edit', 'TextStyle', 'Swatch', 'NoFill', 'StrokeType', 'Links'],
line: ['Edit', 'TextStyle', 'Swatch', 'ArrowMode', 'Links'],
pencil: ['Swatch', 'Links'],
highlighter: ['Swatch', 'Links'],
text: ['Edit', 'Swatch', 'ScaleLevel', 'AutoResizing', 'TextStyle', 'Links'],
text: ['Edit', 'TextStyle', 'Swatch', 'ScaleLevel', 'AutoResizing', 'Links'],
html: ['ScaleLevel', 'AutoResizing', 'Links'],
image: ['Links'],
video: ['Links'],
Expand All @@ -93,6 +88,10 @@ function filterShapeByAction<S extends Shape>(shapes: Shape[], type: ContextBarA
const EditAction = observer(() => {
const app = useApp<Shape>()
const shape = filterShapeByAction(app.selectedShapesArray, 'Edit')[0]
const iconName =
('label' in shape.props && shape.props.label) || ('text' in shape.props && shape.props.text)
? 'forms'
: 'text'

return (
<Button
Expand All @@ -112,7 +111,7 @@ const EditAction = observer(() => {
}
}}
>
<TablerIcon name="text" />
<TablerIcon name={iconName} />
</Button>
)
})
Expand Down
Expand Up @@ -39,7 +39,11 @@ export const GeometryTools = observer(function GeometryTools() {
<Popover.Root>
<Popover.Trigger className="tl-geometry-tools-pane-anchor">
<ToolButton {...geometries.find(geo => geo.id === activeGeomId)!} />
<TablerIcon className="tl-popover-indicator" name="chevron-down-left" />
<TablerIcon
data-selected={geometries.some(geo => geo.id === app.selectedTool.id)}
className="tl-popover-indicator"
name="chevron-down-left"
/>
</Popover.Trigger>

<Popover.Content className="tl-popover-content" side="left" sideOffset={15}>
Expand Down
178 changes: 133 additions & 45 deletions tldraw/apps/tldraw-logseq/src/lib/shapes/BoxShape.tsx
@@ -1,15 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SVGContainer, TLComponentProps, useApp } from '@tldraw/react'
import { TLBoxShape, TLBoxShapeProps, getComputedColor } from '@tldraw/core'
import { SVGContainer, TLComponentProps } from '@tldraw/react'
import { TLBoxShape, TLBoxShapeProps, getComputedColor, getTextLabelSize } from '@tldraw/core'
import Vec from '@tldraw/vec'
import * as React from 'react'
import { observer } from 'mobx-react-lite'
import { CustomStyleProps, withClampedStyles } from './style-props'
import { BindingIndicator } from './BindingIndicator'

import { TextLabel } from './text/TextLabel'
export interface BoxShapeProps extends TLBoxShapeProps, CustomStyleProps {
borderRadius: number
type: 'box'
label: string
fontWeight: number
italic: boolean
}

const font = '18px / 1 var(--ls-font-family)'

export class BoxShape extends TLBoxShape<BoxShapeProps> {
static id = 'box'

Expand All @@ -23,62 +30,143 @@ export class BoxShape extends TLBoxShape<BoxShapeProps> {
stroke: '',
fill: '',
noFill: false,
fontWeight: 400,
italic: false,
strokeType: 'line',
strokeWidth: 2,
opacity: 1,
label: '',
}

ReactComponent = observer(({ events, isErasing, isBinding, isSelected }: TLComponentProps) => {
const {
props: {
size: [w, h],
stroke,
fill,
noFill,
strokeWidth,
strokeType,
borderRadius,
opacity,
},
} = this
canEdit = true

return (
<SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
{isBinding && <BindingIndicator mode="svg" strokeWidth={strokeWidth} size={[w, h]} />}
<rect
className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
x={strokeWidth / 2}
y={strokeWidth / 2}
rx={borderRadius}
ry={borderRadius}
width={Math.max(0.01, w - strokeWidth)}
height={Math.max(0.01, h - strokeWidth)}
pointerEvents="all"
/>
<rect
x={strokeWidth / 2}
y={strokeWidth / 2}
rx={borderRadius}
ry={borderRadius}
width={Math.max(0.01, w - strokeWidth)}
height={Math.max(0.01, h - strokeWidth)}
strokeWidth={strokeWidth}
stroke={getComputedColor(stroke, 'stroke')}
strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
fill={noFill ? 'none' : getComputedColor(fill, 'background')}
/>
</SVGContainer>
)
})
ReactComponent = observer(
({ events, isErasing, isBinding, isSelected, isEditing, onEditingEnd }: TLComponentProps) => {
const {
props: {
size: [w, h],
stroke,
fill,
noFill,
strokeWidth,
strokeType,
borderRadius,
opacity,
label,
italic,
fontWeight,
},
} = this

const labelSize =
label || isEditing
? getTextLabelSize(
label,
{ fontFamily: 'var(--ls-font-family)', fontSize: 18, lineHeight: 1, fontWeight },
4
)
: [0, 0]
const midPoint = Vec.mul(this.props.size, 0.5)
const scale = Math.max(0.5, Math.min(1, w / labelSize[0], h / labelSize[1]))
const bounds = this.getBounds()

const offset = React.useMemo(() => {
return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
}, [bounds, scale, midPoint])

const handleLabelChange = React.useCallback(
(label: string) => {
this.update?.({ label })
},
[label]
)

return (
<div {...events} style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
<TextLabel
font={font}
text={label}
color={getComputedColor(stroke, 'text')}
offsetX={offset[0]}
offsetY={offset[1]}
scale={scale}
isEditing={isEditing}
onChange={handleLabelChange}
onBlur={onEditingEnd}
fontStyle={italic ? 'italic' : 'normal'}
fontWeight={fontWeight}
/>
<SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
{isBinding && <BindingIndicator mode="svg" strokeWidth={strokeWidth} size={[w, h]} />}
<rect
className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
x={strokeWidth / 2}
y={strokeWidth / 2}
rx={borderRadius}
ry={borderRadius}
width={Math.max(0.01, w - strokeWidth)}
height={Math.max(0.01, h - strokeWidth)}
pointerEvents="all"
/>
<rect
x={strokeWidth / 2}
y={strokeWidth / 2}
rx={borderRadius}
ry={borderRadius}
width={Math.max(0.01, w - strokeWidth)}
height={Math.max(0.01, h - strokeWidth)}
strokeWidth={strokeWidth}
stroke={getComputedColor(stroke, 'stroke')}
strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
fill={noFill ? 'none' : getComputedColor(fill, 'background')}
/>
</SVGContainer>
</div>
)
}
)

ReactIndicator = observer(() => {
const {
props: {
size: [w, h],
borderRadius,
label,
fontWeight,
},
} = this
return <rect width={w} height={h} rx={borderRadius} ry={borderRadius} fill="transparent" />

const bounds = this.getBounds()
const labelSize = label
? getTextLabelSize(
label,
{ fontFamily: 'var(--ls-font-family)', fontSize: 18, lineHeight: 1, fontWeight },
4
)
: [0, 0]
const scale = Math.max(0.5, Math.min(1, w / labelSize[0], h / labelSize[1]))
const midPoint = Vec.mul(this.props.size, 0.5)

const offset = React.useMemo(() => {
return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
}, [bounds, scale, midPoint])

return (
<g>
<rect width={w} height={h} rx={borderRadius} ry={borderRadius} fill="transparent" />
{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"
/>
)}
</g>
)
})

validateProps = (props: Partial<BoxShapeProps>) => {
Expand Down

0 comments on commit da0f3eb

Please sign in to comment.