From 0685ca387106dbfb435bcbdd743c822d678b4e1e Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 20 Nov 2021 09:37:42 +0000 Subject: [PATCH] [feature] fonts (#308) * adds fonts * Add alignment options * Update useKeyboardShortcuts.tsx * Improve style panel * Alignment for sticky notes * swap fonts --- .../Primitives/RowButton/RowButton.tsx | 5 +- .../TopPanel/StyleMenu/StyleMenu.tsx | 159 +++++++++++++++--- .../tldraw/src/hooks/useKeyboardShortcuts.tsx | 81 ++++++--- packages/tldraw/src/hooks/useStylesheet.ts | 2 +- packages/tldraw/src/state/TldrawApp.ts | 28 +-- packages/tldraw/src/state/commands/index.ts | 1 + .../state/commands/setShapesProps/index.ts | 1 + .../setShapesProps/setShapesProps.spec.ts | 3 + .../commands/setShapesProps/setShapesProps.ts | 56 ++++++ .../commands/updateShapes/updateShapes.ts | 2 +- packages/tldraw/src/state/data/migrate.ts | 10 +- .../state/shapes/StickyUtil/StickyUtil.tsx | 41 ++++- .../src/state/shapes/TextUtil/TextUtil.tsx | 45 ++++- .../__snapshots__/TextUtil.spec.tsx.snap | 2 + .../src/state/shapes/shared/getTextAlign.ts | 12 ++ .../src/state/shapes/shared/shape-styles.ts | 38 ++++- packages/tldraw/src/types.ts | 17 +- 17 files changed, 420 insertions(+), 83 deletions(-) create mode 100644 packages/tldraw/src/state/commands/setShapesProps/index.ts create mode 100644 packages/tldraw/src/state/commands/setShapesProps/setShapesProps.spec.ts create mode 100644 packages/tldraw/src/state/commands/setShapesProps/setShapesProps.ts create mode 100644 packages/tldraw/src/state/shapes/shared/getTextAlign.ts diff --git a/packages/tldraw/src/components/Primitives/RowButton/RowButton.tsx b/packages/tldraw/src/components/Primitives/RowButton/RowButton.tsx index 32cc6a893..d9b738907 100644 --- a/packages/tldraw/src/components/Primitives/RowButton/RowButton.tsx +++ b/packages/tldraw/src/components/Primitives/RowButton/RowButton.tsx @@ -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', }, diff --git a/packages/tldraw/src/components/TopPanel/StyleMenu/StyleMenu.tsx b/packages/tldraw/src/components/TopPanel/StyleMenu/StyleMenu.tsx index 7a3c62524..f4bb1406d 100644 --- a/packages/tldraw/src/components/TopPanel/StyleMenu/StyleMenu.tsx +++ b/packages/tldraw/src/components/TopPanel/StyleMenu/StyleMenu.tsx @@ -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]: , [DashStyle.Solid]: , [DashStyle.Dashed]: , [DashStyle.Dotted]: , } -const SIZES = { +const SIZE_ICONS = { [SizeStyle.Small]: , [SizeStyle.Medium]: , [SizeStyle.Large]: , } -const themeSelector = (data: TDSnapshot) => (data.settings.isDarkMode ? 'dark' : 'light') +const ALIGN_ICONS = { + [AlignStyle.Start]: , + [AlignStyle.Middle]: , + [AlignStyle.End]: , + [AlignStyle.Justify]: , +} + +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 ( @@ -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]} Color - {Object.keys(strokes.light).map((colorStyle: string) => ( - + {Object.keys(strokes.light).map((style: string) => ( + app.style({ color: colorStyle as ColorStyle })} + isActive={displayedStyle.color === style} + onClick={() => app.style({ color: style as ColorStyle })} > ))} - + + Fill + Dash - {Object.values(DashStyle).map((dashStyle) => ( + {Object.values(DashStyle).map((style) => ( - {DASHES[dashStyle as DashStyle]} + {DASH_ICONS[style as DashStyle]} ))} - Size @@ -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]} ))} - - - Fill - + {showTextStyles && ( + <> + + + Font + + {Object.values(FontStyle).map((fontStyle) => ( + + Aa + + ))} + + + + Align + + {Object.values(AlignStyle).map((style) => ( + + {ALIGN_ICONS[style]} + + ))} + + + + )} ) @@ -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', + }, + }, + }, +}) diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx index 6a851727f..7e20cfc2a 100644 --- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx +++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx @@ -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) { @@ -97,7 +97,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { // 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) { // Focus Mode useHotkeys( - 'ctrl+.,command+.', + 'ctrl+.,⌘+.', () => { if (!canHandleEvent()) return app.toggleFocusMode() @@ -124,7 +124,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { 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) { [app] ) useHotkeys( - 'ctrl+s,command+s', + 'ctrl+s,⌘+s', (e) => { if (!canHandleEvent()) return @@ -145,7 +145,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { ) 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) { [app] ) useHotkeys( - 'ctrl+o,command+o', + 'ctrl+o,⌘+o', (e) => { if (!canHandleEvent()) return @@ -168,10 +168,12 @@ export function useKeyboardShortcuts(ref: React.RefObject) { // 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) { ) 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) { // 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) { ) 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) { // Camera useHotkeys( - 'ctrl+=,command+=', + 'ctrl+=,⌘+=', (e) => { if (!canHandleEvent()) return @@ -236,7 +238,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { ) useHotkeys( - 'ctrl+-,command+-', + 'ctrl+-,⌘+-', (e) => { if (!canHandleEvent()) return @@ -280,7 +282,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { // Duplicate useHotkeys( - 'ctrl+d,command+d', + 'ctrl+d,⌘+d', (e) => { if (!canHandleEvent()) return @@ -341,7 +343,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { // 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) { ) 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) { // 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) { ) useHotkeys( - 'command+x,ctrl+x', + '⌘+x,ctrl+x', () => { if (!canHandleEvent()) return app.cut() @@ -465,7 +467,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { ) useHotkeys( - 'command+v,ctrl+v', + '⌘+v,ctrl+v', () => { if (!canHandleEvent()) return app.paste() @@ -477,7 +479,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { // Group & Ungroup useHotkeys( - 'command+g,ctrl+g', + '⌘+g,ctrl+g', (e) => { if (!canHandleEvent()) return @@ -489,7 +491,7 @@ export function useKeyboardShortcuts(ref: React.RefObject) { ) 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) { ) 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) { 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] + ) } diff --git a/packages/tldraw/src/hooks/useStylesheet.ts b/packages/tldraw/src/hooks/useStylesheet.ts index 3806c99b2..9e46c38a0 100644 --- a/packages/tldraw/src/hooks/useStylesheet.ts +++ b/packages/tldraw/src/hooks/useStylesheet.ts @@ -4,7 +4,7 @@ const styles = new Map() 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() { diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index 93a85b685..96b5bd272 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -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 { * @protected * @returns The final state */ - protected cleanup = ( - state: TDSnapshot, - prev: TDSnapshot, - patch: Patch - ): 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 { 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 { 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 { 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 = (props: Partial, 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 { 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 { 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, diff --git a/packages/tldraw/src/state/commands/index.ts b/packages/tldraw/src/state/commands/index.ts index e01365a46..3fd73c8e4 100644 --- a/packages/tldraw/src/state/commands/index.ts +++ b/packages/tldraw/src/state/commands/index.ts @@ -21,3 +21,4 @@ export * from './toggleShapesProp' export * from './translateShapes' export * from './ungroupShapes' export * from './updateShapes' +export * from './setShapesProps' diff --git a/packages/tldraw/src/state/commands/setShapesProps/index.ts b/packages/tldraw/src/state/commands/setShapesProps/index.ts new file mode 100644 index 000000000..55fb0cd0c --- /dev/null +++ b/packages/tldraw/src/state/commands/setShapesProps/index.ts @@ -0,0 +1 @@ +export * from './setShapesProps' diff --git a/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.spec.ts b/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.spec.ts new file mode 100644 index 000000000..9f779494e --- /dev/null +++ b/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.spec.ts @@ -0,0 +1,3 @@ +describe('Set shapes props command', () => { + it.todo('sets the props of the provided shapes') +}) diff --git a/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.ts b/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.ts new file mode 100644 index 000000000..12144c0a3 --- /dev/null +++ b/packages/tldraw/src/state/commands/setShapesProps/setShapesProps.ts @@ -0,0 +1,56 @@ +import type { TDShape, TldrawCommand } from '~types' +import type { TldrawApp } from '~state' + +export function setShapesProps( + app: TldrawApp, + ids: string[], + partial: Partial +): TldrawCommand { + const { currentPageId, selectedIds } = app + + const initialShapes = ids + .map((id) => app.getShape(id)) + .filter((shape) => (partial['isLocked'] ? true : !shape.isLocked)) + + const before: Record> = {} + const after: Record> = {} + + 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, + }, + }, + }, + }, + } +} diff --git a/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts b/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts index 0c9141f05..4c71d4b23 100644 --- a/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts +++ b/packages/tldraw/src/state/commands/updateShapes/updateShapes.ts @@ -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)[], pageId: string diff --git a/packages/tldraw/src/state/data/migrate.ts b/packages/tldraw/src/state/data/migrate.ts index 041ea5487..e6d84c733 100644 --- a/packages/tldraw/src/state/data/migrate.ts +++ b/packages/tldraw/src/state/data/migrate.ts @@ -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) => { diff --git a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx index 162dbf31f..b1b54dc47 100644 --- a/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx +++ b/packages/tldraw/src/state/shapes/StickyUtil/StickyUtil.tsx @@ -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 { size: [200, 200], text: '', rotation: 0, - style: defaultStyle, + style: defaultTextStyle, }, props ) @@ -165,7 +165,7 @@ export class StickyUtil extends TDShapeUtil { isGhost={isGhost} style={{ backgroundColor: fill, ...style }} > - + {shape.text}​ {isEditing && ( @@ -184,6 +184,7 @@ export class StickyUtil extends TDShapeUtil { autoSave="false" autoFocus spellCheck={false} + alignment={shape.style.textAlign} /> )} @@ -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', + }, + }, + }, }) diff --git a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx index 3baf3efbc..cf04cbec5 100644 --- a/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx +++ b/packages/tldraw/src/state/shapes/TextUtil/TextUtil.tsx @@ -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 { point: [0, 0], rotation: 0, text: ' ', - style: defaultStyle, + style: defaultTextStyle, }, props ) @@ -50,7 +51,39 @@ export class TextUtil extends TDShapeUtil { const handleChange = React.useCallback( (e: React.ChangeEvent) => { - 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 { style={{ font, color: styles.stroke, + textAlign: getTextAlign(style.textAlign), }} > {isBinding && ( @@ -147,6 +181,7 @@ export class TextUtil extends TDShapeUtil { style={{ font, color: styles.stroke, + textAlign: 'inherit', }} name="text" defaultValue={text} diff --git a/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap b/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap index d79d3a67f..15f3e1e66 100644 --- a/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap +++ b/packages/tldraw/src/state/shapes/TextUtil/__snapshots__/TextUtil.spec.tsx.snap @@ -14,9 +14,11 @@ Object { "style": Object { "color": "black", "dash": "draw", + "font": "script", "isFilled": false, "scale": 1, "size": "small", + "textAlign": "start", }, "text": " ", "type": "text", diff --git a/packages/tldraw/src/state/shapes/shared/getTextAlign.ts b/packages/tldraw/src/state/shapes/shared/getTextAlign.ts new file mode 100644 index 000000000..1adf007f0 --- /dev/null +++ b/packages/tldraw/src/state/shapes/shared/getTextAlign.ts @@ -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] +} diff --git a/packages/tldraw/src/state/shapes/shared/shape-styles.ts b/packages/tldraw/src/state/shapes/shared/shape-styles.ts index 14730daa9..b2d999455 100644 --- a/packages/tldraw/src/state/shapes/shared/shape-styles.ts +++ b/packages/tldraw/src/state/shapes/shared/shape-styles.ts @@ -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, +} diff --git a/packages/tldraw/src/types.ts b/packages/tldraw/src/types.ts index 53ab0a554..a45e38d06 100644 --- a/packages/tldraw/src/types.ts +++ b/packages/tldraw/src/types.ts @@ -94,7 +94,6 @@ export interface TDSnapshot { appState: { currentStyle: ShapeStyles currentPageId: string - pages: Pick, '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 }