[feature] fonts (#308)

* adds fonts

* Add alignment options

* Update useKeyboardShortcuts.tsx

* Improve style panel

* Alignment for sticky notes

* swap fonts
This commit is contained in:
Steve Ruiz 2021-11-20 09:37:42 +00:00 committed by GitHub
parent a0891ca3ff
commit 0685ca3871
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 420 additions and 83 deletions

View file

@ -11,7 +11,7 @@ export interface RowButtonProps {
children: React.ReactNode
disabled?: boolean
kbd?: string
variant?: 'wide'
variant?: 'wide' | 'styleMenu'
isSponsor?: boolean
isActive?: boolean
isWarning?: boolean
@ -130,6 +130,9 @@ export const StyledRowButton = styled('button', {
small: {},
},
variant: {
styleMenu: {
margin: '$1 0 $1 0',
},
wide: {
gridColumn: '1 / span 4',
},

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { strokes, fills, defaultStyle } from '~state/shapes/shared/shape-styles'
import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles'
import { useTldrawApp } from '~hooks'
import {
DMCheckboxItem,
@ -19,37 +19,65 @@ import {
SizeSmallIcon,
} from '~components/Primitives/icons'
import { ToolButton } from '~components/Primitives/ToolButton'
import { TDSnapshot, ColorStyle, DashStyle, SizeStyle, ShapeStyles } from '~types'
import {
TDSnapshot,
ColorStyle,
DashStyle,
SizeStyle,
ShapeStyles,
FontStyle,
AlignStyle,
} from '~types'
import { styled } from '~styles'
import { breakpoints } from '~components/breakpoints'
import { Divider } from '~components/Primitives/Divider'
import { preventEvent } from '~components/preventEvent'
import {
TextAlignCenterIcon,
TextAlignJustifyIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from '@radix-ui/react-icons'
const currentStyleSelector = (s: TDSnapshot) => s.appState.currentStyle
const selectedIdsSelector = (s: TDSnapshot) =>
s.document.pageStates[s.appState.currentPageId].selectedIds
const STYLE_KEYS = Object.keys(defaultStyle) as (keyof ShapeStyles)[]
const STYLE_KEYS = Object.keys(defaultTextStyle) as (keyof ShapeStyles)[]
const DASHES = {
const DASH_ICONS = {
[DashStyle.Draw]: <DashDrawIcon />,
[DashStyle.Solid]: <DashSolidIcon />,
[DashStyle.Dashed]: <DashDashedIcon />,
[DashStyle.Dotted]: <DashDottedIcon />,
}
const SIZES = {
const SIZE_ICONS = {
[SizeStyle.Small]: <SizeSmallIcon />,
[SizeStyle.Medium]: <SizeMediumIcon />,
[SizeStyle.Large]: <SizeLargeIcon />,
}
const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light')
const ALIGN_ICONS = {
[AlignStyle.Start]: <TextAlignLeftIcon />,
[AlignStyle.Middle]: <TextAlignCenterIcon />,
[AlignStyle.End]: <TextAlignRightIcon />,
[AlignStyle.Justify]: <TextAlignJustifyIcon />,
}
const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light')
const showTextStylesSelector = (s: TDSnapshot) => {
const pageId = s.appState.currentPageId
const page = s.document.pages[pageId]
return s.document.pageStates[pageId].selectedIds.some((id) => 'text' in page.shapes[id])
}
export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
const app = useTldrawApp()
const theme = app.useStore(themeSelector)
const showTextStyles = app.useStore(showTextStylesSelector)
const currentStyle = app.useStore(currentStyleSelector)
const selectedIds = app.useStore(selectedIdsSelector)
@ -111,6 +139,14 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
app.style({ size: value as SizeStyle })
}, [])
const handleFontChange = React.useCallback((value: string) => {
app.style({ font: value as FontStyle })
}, [])
const handleTextAlignChange = React.useCallback((value: string) => {
app.style({ textAlign: value as AlignStyle })
}, [])
return (
<DropdownMenu.Root dir="ltr">
<DMTriggerIcon>
@ -126,53 +162,56 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
fill={fills[theme][displayedStyle.color as ColorStyle]}
/>
)}
{DASHES[displayedStyle.dash]}
{DASH_ICONS[displayedStyle.dash]}
</OverlapIcons>
</DMTriggerIcon>
<DMContent>
<StyledRow variant="tall">
<span>Color</span>
<ColorGrid>
{Object.keys(strokes.light).map((colorStyle: string) => (
<DropdownMenu.Item key={colorStyle} onSelect={preventEvent} asChild>
{Object.keys(strokes.light).map((style: string) => (
<DropdownMenu.Item key={style} onSelect={preventEvent} asChild>
<ToolButton
variant="icon"
isActive={displayedStyle.color === colorStyle}
onClick={() => app.style({ color: colorStyle as ColorStyle })}
isActive={displayedStyle.color === style}
onClick={() => app.style({ color: style as ColorStyle })}
>
<CircleIcon
size={18}
strokeWidth={2.5}
fill={
displayedStyle.isFilled
? fills.light[colorStyle as ColorStyle]
: 'transparent'
displayedStyle.isFilled ? fills.light[style as ColorStyle] : 'transparent'
}
stroke={strokes.light[colorStyle as ColorStyle]}
stroke={strokes.light[style as ColorStyle]}
/>
</ToolButton>
</DropdownMenu.Item>
))}
</ColorGrid>
</StyledRow>
<Divider />
<DMCheckboxItem
variant="styleMenu"
checked={!!displayedStyle.isFilled}
onCheckedChange={handleToggleFilled}
>
Fill
</DMCheckboxItem>
<StyledRow>
Dash
<StyledGroup dir="ltr" value={displayedStyle.dash} onValueChange={handleDashChange}>
{Object.values(DashStyle).map((dashStyle) => (
{Object.values(DashStyle).map((style) => (
<DMRadioItem
key={dashStyle}
isActive={dashStyle === displayedStyle.dash}
value={dashStyle}
key={style}
isActive={style === displayedStyle.dash}
value={style}
onSelect={preventEvent}
bp={breakpoints}
>
{DASHES[dashStyle as DashStyle]}
{DASH_ICONS[style as DashStyle]}
</DMRadioItem>
))}
</StyledGroup>
</StyledRow>
<Divider />
<StyledRow>
Size
<StyledGroup dir="ltr" value={displayedStyle.size} onValueChange={handleSizeChange}>
@ -184,15 +223,52 @@ export const StyleMenu = React.memo(function ColorMenu(): JSX.Element {
onSelect={preventEvent}
bp={breakpoints}
>
{SIZES[sizeStyle as SizeStyle]}
{SIZE_ICONS[sizeStyle as SizeStyle]}
</DMRadioItem>
))}
</StyledGroup>
</StyledRow>
<Divider />
<DMCheckboxItem checked={!!displayedStyle.isFilled} onCheckedChange={handleToggleFilled}>
Fill
</DMCheckboxItem>
{showTextStyles && (
<>
<Divider />
<StyledRow>
Font
<StyledGroup dir="ltr" value={displayedStyle.font} onValueChange={handleFontChange}>
{Object.values(FontStyle).map((fontStyle) => (
<DMRadioItem
key={fontStyle}
isActive={fontStyle === displayedStyle.font}
value={fontStyle}
onSelect={preventEvent}
bp={breakpoints}
>
<FontIcon fontStyle={fontStyle}>Aa</FontIcon>
</DMRadioItem>
))}
</StyledGroup>
</StyledRow>
<StyledRow>
Align
<StyledGroup
dir="ltr"
value={displayedStyle.textAlign}
onValueChange={handleTextAlignChange}
>
{Object.values(AlignStyle).map((style) => (
<DMRadioItem
key={style}
isActive={style === displayedStyle.textAlign}
value={style}
onSelect={preventEvent}
bp={breakpoints}
>
{ALIGN_ICONS[style]}
</DMRadioItem>
))}
</StyledGroup>
</StyledRow>
</>
)}
</DMContent>
</DropdownMenu.Root>
)
@ -237,7 +313,7 @@ export const StyledRow = styled('div', {
fontFamily: '$ui',
fontWeight: 400,
fontSize: '$1',
padding: '0 0 0 $3',
padding: '$2 0 $2 $3',
borderRadius: 4,
userSelect: 'none',
margin: 0,
@ -250,6 +326,7 @@ export const StyledRow = styled('div', {
variant: {
tall: {
alignItems: 'flex-start',
padding: '0 0 0 $3',
'& > span': {
paddingTop: '$4',
},
@ -261,6 +338,7 @@ export const StyledRow = styled('div', {
const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, {
display: 'flex',
flexDirection: 'row',
gap: '$1',
})
const OverlapIcons = styled('div', {
@ -270,3 +348,28 @@ const OverlapIcons = styled('div', {
gridRow: 1,
},
})
const FontIcon = styled('div', {
width: 32,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '$3',
variants: {
fontStyle: {
[FontStyle.Script]: {
fontFamily: 'Caveat Brush',
},
[FontStyle.Sans]: {
fontFamily: 'Recursive',
},
[FontStyle.Serif]: {
fontFamily: 'Georgia',
},
[FontStyle.Mono]: {
fontFamily: 'Recursive Mono',
},
},
},
})

View file

@ -1,6 +1,6 @@
import * as React from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { TDShapeType } from '~types'
import { AlignStyle, TDShapeType } from '~types'
import { useFileSystemHandlers, useTldrawApp } from '~hooks'
export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
@ -97,7 +97,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Dark Mode
useHotkeys(
'ctrl+shift+d,command+shift+d',
'ctrl+shift+d,+shift+d',
(e) => {
if (!canHandleEvent()) return
app.toggleDarkMode()
@ -110,7 +110,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Focus Mode
useHotkeys(
'ctrl+.,command+.',
'ctrl+.,+.',
() => {
if (!canHandleEvent()) return
app.toggleFocusMode()
@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
const { onNewProject, onOpenProject, onSaveProject, onSaveProjectAs } = useFileSystemHandlers()
useHotkeys(
'ctrl+n,command+n',
'ctrl+n,+n',
(e) => {
if (!canHandleEvent()) return
@ -134,7 +134,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
[app]
)
useHotkeys(
'ctrl+s,command+s',
'ctrl+s,+s',
(e) => {
if (!canHandleEvent()) return
@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'ctrl+shift+s,command+shift+s',
'ctrl+shift+s,+shift+s',
(e) => {
if (!canHandleEvent()) return
@ -155,7 +155,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
[app]
)
useHotkeys(
'ctrl+o,command+o',
'ctrl+o,+o',
(e) => {
if (!canHandleEvent()) return
@ -168,10 +168,12 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Undo Redo
useHotkeys(
'command+z,ctrl+z',
'+z,ctrl+z',
() => {
if (!canHandleEvent()) return
console.log('Hello')
if (app.session) {
app.cancelSession()
} else {
@ -183,7 +185,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'ctrl+shift-z,command+shift+z',
'ctrl+shift-z,+shift+z',
() => {
if (!canHandleEvent()) return
@ -200,7 +202,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Undo Redo
useHotkeys(
'command+u,ctrl+u',
'+u,ctrl+u',
() => {
if (!canHandleEvent()) return
app.undoSelect()
@ -210,7 +212,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'ctrl+shift-u,command+shift+u',
'ctrl+shift-u,+shift+u',
() => {
if (!canHandleEvent()) return
app.redoSelect()
@ -224,7 +226,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Camera
useHotkeys(
'ctrl+=,command+=',
'ctrl+=,+=',
(e) => {
if (!canHandleEvent()) return
@ -236,7 +238,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'ctrl+-,command+-',
'ctrl+-,+-',
(e) => {
if (!canHandleEvent()) return
@ -280,7 +282,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Duplicate
useHotkeys(
'ctrl+d,command+d',
'ctrl+d,+d',
(e) => {
if (!canHandleEvent()) return
@ -341,7 +343,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Select All
useHotkeys(
'command+a,ctrl+a',
'+a,ctrl+a',
() => {
if (!canHandleEvent()) return
app.selectAll()
@ -433,7 +435,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'command+shift+l,ctrl+shift+l',
'+shift+l,ctrl+shift+l',
() => {
if (!canHandleEvent()) return
app.toggleLocked()
@ -445,7 +447,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Copy, Cut & Paste
useHotkeys(
'command+c,ctrl+c',
'+c,ctrl+c',
() => {
if (!canHandleEvent()) return
app.copy()
@ -455,7 +457,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'command+x,ctrl+x',
'+x,ctrl+x',
() => {
if (!canHandleEvent()) return
app.cut()
@ -465,7 +467,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'command+v,ctrl+v',
'+v,ctrl+v',
() => {
if (!canHandleEvent()) return
app.paste()
@ -477,7 +479,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
// Group & Ungroup
useHotkeys(
'command+g,ctrl+g',
'+g,ctrl+g',
(e) => {
if (!canHandleEvent()) return
@ -489,7 +491,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'command+shift+g,ctrl+shift+g',
'+shift+g,ctrl+shift+g',
(e) => {
if (!canHandleEvent()) return
@ -543,7 +545,7 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
)
useHotkeys(
'command+shift+backspace',
'ctrl+shift+backspace,⌘+shift+backspace',
(e) => {
if (!canHandleEvent()) return
if (app.settings.isDebugMode) {
@ -554,4 +556,39 @@ export function useKeyboardShortcuts(ref: React.RefObject<HTMLDivElement>) {
undefined,
[app]
)
// Text Align
useHotkeys(
'alt+command+l,alt+ctrl+l',
(e) => {
if (!canHandleEvent()) return
app.style({ textAlign: AlignStyle.Start })
e.preventDefault()
},
undefined,
[app]
)
useHotkeys(
'alt+command+t,alt+ctrl+t',
(e) => {
if (!canHandleEvent()) return
app.style({ textAlign: AlignStyle.Middle })
e.preventDefault()
},
undefined,
[app]
)
useHotkeys(
'alt+command+r,alt+ctrl+r',
(e) => {
if (!canHandleEvent()) return
app.style({ textAlign: AlignStyle.End })
e.preventDefault()
},
undefined,
[app]
)
}

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&display=swap')
@import url('https://fonts.googleapis.com/css2?family=Caveat+Brush&family=Source+Code+Pro&family=Source+Sans+Pro&family=Source+Serif+Pro&display=swap');
`
export function useStylesheet() {

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Patch, StateManager } from 'rko'
import { StateManager } from 'rko'
import { Vec } from '@tldraw/vec'
import {
TLBoundsEventHandler,
@ -247,11 +247,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
* @protected
* @returns The final state
*/
protected cleanup = (
state: TDSnapshot,
prev: TDSnapshot,
patch: Patch<TDSnapshot>
): TDSnapshot => {
protected cleanup = (state: TDSnapshot, prev: TDSnapshot): TDSnapshot => {
const next = { ...state }
// Remove deleted shapes and bindings (in Commands, these will be set to undefined)
@ -2064,7 +2060,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
if (shapesToUpdate.length === 0) return this
return this.setState(
Commands.update(this, shapesToUpdate, this.currentPageId),
Commands.updateShapes(this, shapesToUpdate, this.currentPageId),
'updated_shapes'
)
}
@ -2079,7 +2075,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
const shapesToUpdate = shapes.filter((shape) => pageShapes[shape.id])
if (shapesToUpdate.length === 0) return this
return this.patchState(
Commands.update(this, shapesToUpdate, this.currentPageId).after,
Commands.updateShapes(this, shapesToUpdate, this.currentPageId).after,
'updated_shapes'
)
}
@ -2333,6 +2329,15 @@ export class TldrawApp extends StateManager<TDSnapshot> {
return this.setState(Commands.toggleShapesDecoration(this, ids, handleId))
}
/**
* Set the props of one or more shapes
* @param props The props to set on the shapes.
* @param ids The ids of the shapes to set props on.
*/
setShapeProps = <T extends TDShape>(props: Partial<T>, ids = this.selectedIds) => {
return this.setState(Commands.setShapesProps(this, ids, props))
}
/**
* Rotate one or more shapes by a delta.
* @param delta The delta in radians.
@ -2800,12 +2805,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
getShapeUtil = TLDR.getShapeUtil
static version = 13
static version = 14
static defaultDocument: TDDocument = {
id: 'doc',
name: 'New Document',
version: 13,
version: 14,
pages: {
page: {
id: 'page',
@ -2843,15 +2848,14 @@ export class TldrawApp extends StateManager<TDSnapshot> {
showCloneHandles: false,
},
appState: {
status: TDStatus.Idle,
activeTool: 'select',
hoveredId: undefined,
currentPageId: 'page',
pages: [{ id: 'page', name: 'page', childIndex: 1 }],
currentStyle: defaultStyle,
isToolLocked: false,
isStyleOpen: false,
isEmptyCanvas: false,
status: TDStatus.Idle,
snapLines: [],
},
document: TldrawApp.defaultDocument,

View file

@ -21,3 +21,4 @@ export * from './toggleShapesProp'
export * from './translateShapes'
export * from './ungroupShapes'
export * from './updateShapes'
export * from './setShapesProps'

View file

@ -0,0 +1 @@
export * from './setShapesProps'

View file

@ -0,0 +1,3 @@
describe('Set shapes props command', () => {
it.todo('sets the props of the provided shapes')
})

View file

@ -0,0 +1,56 @@
import type { TDShape, TldrawCommand } from '~types'
import type { TldrawApp } from '~state'
export function setShapesProps<T extends TDShape>(
app: TldrawApp,
ids: string[],
partial: Partial<T>
): TldrawCommand {
const { currentPageId, selectedIds } = app
const initialShapes = ids
.map((id) => app.getShape<T>(id))
.filter((shape) => (partial['isLocked'] ? true : !shape.isLocked))
const before: Record<string, Partial<TDShape>> = {}
const after: Record<string, Partial<TDShape>> = {}
const keys = Object.keys(partial) as (keyof T)[]
initialShapes.forEach((shape) => {
before[shape.id] = Object.fromEntries(keys.map((key) => [key, shape[key]]))
after[shape.id] = partial
})
return {
id: 'set_props',
before: {
document: {
pages: {
[currentPageId]: {
shapes: before,
},
},
pageStates: {
[currentPageId]: {
selectedIds,
},
},
},
},
after: {
document: {
pages: {
[currentPageId]: {
shapes: after,
},
},
pageStates: {
[currentPageId]: {
selectedIds,
},
},
},
},
}
}

View file

@ -2,7 +2,7 @@ import type { TldrawCommand, TDShape } from '~types'
import { TLDR } from '~state/TLDR'
import type { TldrawApp } from '../../internal'
export function update(
export function updateShapes(
app: TldrawApp,
updates: ({ id: string } & Partial<TDShape>)[],
pageId: string

View file

@ -1,11 +1,19 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Decoration, TDDocument, TDShapeType } from '~types'
import { Decoration, FontStyle, TDDocument, TDShapeType, TextShape } from '~types'
export function migrate(document: TDDocument, newVersion: number): TDDocument {
const { version = 0 } = document
if (version === newVersion) return document
if (version < 14) {
Object.values(document.pages).forEach((page) => {
Object.values(page.shapes)
.filter((shape) => shape.type === TDShapeType.Text)
.forEach((shape) => (shape as TextShape).style.font === FontStyle.Script)
})
}
// Lowercase styles, move binding meta to binding
if (version <= 13) {
Object.values(document.pages).forEach((page) => {

View file

@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
import { defaultStyle } from '../shared/shape-styles'
import { StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
import { defaultTextStyle } from '../shared/shape-styles'
import { AlignStyle, StickyShape, TDMeta, TDShapeType, TransformInfo } from '~types'
import { getBoundsRectangle, TextAreaUtils } from '../shared'
import { TDShapeUtil } from '../TDShapeUtil'
import { getStickyFontStyle, getStickyShapeStyle } from '../shared/shape-styles'
import { styled } from '~styles'
import Vec from '@tldraw/vec'
import { Vec } from '@tldraw/vec'
import { GHOSTED_OPACITY } from '~constants'
import { TLDR } from '~state/TLDR'
@ -35,7 +35,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
size: [200, 200],
text: '',
rotation: 0,
style: defaultStyle,
style: defaultTextStyle,
},
props
)
@ -165,7 +165,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
isGhost={isGhost}
style={{ backgroundColor: fill, ...style }}
>
<StyledText ref={rText} isEditing={isEditing}>
<StyledText ref={rText} isEditing={isEditing} alignment={shape.style.textAlign}>
{shape.text}&#8203;
</StyledText>
{isEditing && (
@ -184,6 +184,7 @@ export class StickyUtil extends TDShapeUtil<T, E> {
autoSave="false"
autoFocus
spellCheck={false}
alignment={shape.style.textAlign}
/>
)}
</StyledStickyContainer>
@ -291,6 +292,20 @@ const StyledText = styled('div', {
opacity: 1,
},
},
alignment: {
[AlignStyle.Start]: {
textAlign: 'left',
},
[AlignStyle.Middle]: {
textAlign: 'center',
},
[AlignStyle.End]: {
textAlign: 'right',
},
[AlignStyle.Justify]: {
textAlign: 'justify',
},
},
},
...commonTextWrapping,
})
@ -310,4 +325,20 @@ const StyledTextArea = styled('textarea', {
resize: 'none',
caretColor: 'black',
...commonTextWrapping,
variants: {
alignment: {
[AlignStyle.Start]: {
textAlign: 'left',
},
[AlignStyle.Middle]: {
textAlign: 'center',
},
[AlignStyle.End]: {
textAlign: 'right',
},
[AlignStyle.Justify]: {
textAlign: 'justify',
},
},
},
})

View file

@ -1,14 +1,15 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import * as React from 'react'
import { Utils, HTMLContainer, TLBounds } from '@tldraw/core'
import { defaultStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
import { TextShape, TDMeta, TDShapeType, TransformInfo } from '~types'
import { defaultTextStyle, getShapeStyle, getFontStyle } from '../shared/shape-styles'
import { TextShape, TDMeta, TDShapeType, TransformInfo, AlignStyle } from '~types'
import { TextAreaUtils } from '../shared'
import { BINDING_DISTANCE, GHOSTED_OPACITY } from '~constants'
import { TDShapeUtil } from '../TDShapeUtil'
import { styled } from '~styles'
import Vec from '@tldraw/vec'
import { Vec } from '@tldraw/vec'
import { TLDR } from '~state/TLDR'
import { getTextAlign } from '../shared/getTextAlign'
type T = TextShape
type E = HTMLDivElement
@ -33,7 +34,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
point: [0, 0],
rotation: 0,
text: ' ',
style: defaultStyle,
style: defaultTextStyle,
},
props
)
@ -50,7 +51,39 @@ export class TextUtil extends TDShapeUtil<T, E> {
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
onShapeChange?.({ ...shape, text: TLDR.normalizeText(e.currentTarget.value) })
let delta = [0, 0]
const currentBounds = this.getBounds(shape)
switch (shape.style.textAlign) {
case AlignStyle.Start: {
break
}
case AlignStyle.Middle: {
const nextBounds = this.getBounds({
...shape,
text: TLDR.normalizeText(e.currentTarget.value),
})
delta = Vec.div([nextBounds.width - currentBounds.width, 0], 2)
break
}
case AlignStyle.End: {
const nextBounds = this.getBounds({
...shape,
text: TLDR.normalizeText(e.currentTarget.value),
})
delta = [nextBounds.width - currentBounds.width, 0]
break
}
}
onShapeChange?.({
...shape,
point: Vec.sub(shape.point, delta),
text: TLDR.normalizeText(e.currentTarget.value),
})
},
[shape]
)
@ -126,6 +159,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
style={{
font,
color: styles.stroke,
textAlign: getTextAlign(style.textAlign),
}}
>
{isBinding && (
@ -147,6 +181,7 @@ export class TextUtil extends TDShapeUtil<T, E> {
style={{
font,
color: styles.stroke,
textAlign: 'inherit',
}}
name="text"
defaultValue={text}

View file

@ -14,9 +14,11 @@ Object {
"style": Object {
"color": "black",
"dash": "draw",
"font": "script",
"isFilled": false,
"scale": 1,
"size": "small",
"textAlign": "start",
},
"text": " ",
"type": "text",

View file

@ -0,0 +1,12 @@
import { AlignStyle } from '~types'
const ALIGN_VALUES = {
[AlignStyle.Start]: 'left',
[AlignStyle.Middle]: 'center',
[AlignStyle.End]: 'right',
[AlignStyle.Justify]: 'justify',
} as const
export function getTextAlign(alignStyle: AlignStyle = AlignStyle.Start) {
return ALIGN_VALUES[alignStyle]
}

View file

@ -1,5 +1,5 @@
import { Utils } from '@tldraw/core'
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from '~types'
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle, FontStyle, AlignStyle } from '~types'
const canvasLight = '#fafafa'
@ -84,6 +84,20 @@ const fontSizes = {
auto: 'auto',
}
const fontFaces = {
[FontStyle.Script]: '"Caveat Brush"',
[FontStyle.Sans]: '"Source Sans Pro", sans-serif',
[FontStyle.Serif]: '"Source Serif Pro", serif',
[FontStyle.Mono]: '"Source Code Pro", monospace',
}
const fontSizeModifiers = {
[FontStyle.Script]: 1,
[FontStyle.Sans]: 1,
[FontStyle.Serif]: 1,
[FontStyle.Mono]: 1,
}
const stickyFontSizes = {
[SizeStyle.Small]: 24,
[SizeStyle.Medium]: 36,
@ -95,8 +109,12 @@ export function getStrokeWidth(size: SizeStyle): number {
return strokeWidths[size]
}
export function getFontSize(size: SizeStyle): number {
return fontSizes[size]
export function getFontSize(size: SizeStyle, fontStyle: FontStyle = FontStyle.Script): number {
return fontSizes[size] * fontSizeModifiers[fontStyle]
}
export function getFontFace(font: FontStyle = FontStyle.Script): string {
return fontFaces[font]
}
export function getStickyFontSize(size: SizeStyle): number {
@ -104,17 +122,19 @@ export function getStickyFontSize(size: SizeStyle): number {
}
export function getFontStyle(style: ShapeStyles): string {
const fontSize = getFontSize(style.size)
const fontSize = getFontSize(style.size, style.font)
const fontFace = getFontFace(style.font)
const { scale = 1 } = style
return `${fontSize * scale}px/1.3 "Caveat Brush"`
return `${fontSize * scale}px/1.3 ${fontFace}`
}
export function getStickyFontStyle(style: ShapeStyles): string {
const fontSize = getStickyFontSize(style.size)
const fontFace = getFontFace(style.font)
const { scale = 1 } = style
return `${fontSize * scale}px/1.3 "Caveat Brush"`
return `${fontSize * scale}px/1.3 ${fontFace}`
}
export function getStickyShapeStyle(style: ShapeStyles, isDarkMode = false) {
@ -158,3 +178,9 @@ export const defaultStyle: ShapeStyles = {
dash: DashStyle.Draw,
scale: 1,
}
export const defaultTextStyle: ShapeStyles = {
...defaultStyle,
font: FontStyle.Script,
textAlign: AlignStyle.Start,
}

View file

@ -94,7 +94,6 @@ export interface TDSnapshot {
appState: {
currentStyle: ShapeStyles
currentPageId: string
pages: Pick<TLPage<TDShape, TDBinding>, 'id' | 'name' | 'childIndex'>[]
hoveredId?: string
activeTool: TDToolType
isToolLocked: boolean
@ -401,10 +400,26 @@ export enum FontSize {
ExtraLarge = 'extraLarge',
}
export enum AlignStyle {
Start = 'start',
Middle = 'middle',
End = 'end',
Justify = 'justify',
}
export enum FontStyle {
Script = 'script',
Sans = 'sans',
Serif = 'erif',
Mono = 'mono',
}
export type ShapeStyles = {
color: ColorStyle
size: SizeStyle
dash: DashStyle
font?: FontStyle
textAlign?: AlignStyle
isFilled?: boolean
scale?: number
}