import * as React from 'react' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { strokes, fills, defaultTextStyle } from '~state/shapes/shared/shape-styles' import { FormattedMessage } from 'react-intl' import { useTldrawApp } from '~hooks' import { DMCheckboxItem, DMContent, DMDivider, DMRadioItem, } from '~components/Primitives/DropdownMenu' import { CircleIcon, DashDashedIcon, DashDottedIcon, DashDrawIcon, DashSolidIcon, SizeLargeIcon, SizeMediumIcon, SizeSmallIcon, } from '~components/Primitives/icons' import { ToolButton } from '~components/Primitives/ToolButton' import { TDSnapshot, ColorStyle, DashStyle, SizeStyle, ShapeStyles, FontStyle, AlignStyle, TDShapeType, } 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(defaultTextStyle) as (keyof ShapeStyles)[] const DASH_ICONS = { [DashStyle.Draw]: , [DashStyle.Solid]: , [DashStyle.Dashed]: , [DashStyle.Dotted]: , } const SIZE_ICONS = { [SizeStyle.Small]: , [SizeStyle.Medium]: , [SizeStyle.Large]: , } const ALIGN_ICONS = { [AlignStyle.Start]: , [AlignStyle.Middle]: , [AlignStyle.End]: , [AlignStyle.Justify]: , } const themeSelector = (s: TDSnapshot) => (s.settings.isDarkMode ? 'dark' : 'light') const keepOpenSelector = (s: TDSnapshot) => s.settings.keepStyleMenuOpen const optionsSelector = (s: TDSnapshot) => { const { activeTool, currentPageId: pageId } = s.appState switch (activeTool) { case 'select': { const page = s.document.pages[pageId] let hasText = false let hasLabel = false for (const id of s.document.pageStates[pageId].selectedIds) { if ('text' in page.shapes[id]) hasText = true if ('label' in page.shapes[id]) hasLabel = true } return hasText ? 'text' : hasLabel ? 'label' : '' } case TDShapeType.Text: { return 'text' } case TDShapeType.Rectangle: { return 'label' } case TDShapeType.Ellipse: { return 'label' } case TDShapeType.Triangle: { return 'label' } case TDShapeType.Arrow: { return 'label' } case TDShapeType.Line: { return 'label' } } return false } export const StyleMenu = React.memo(function ColorMenu() { const app = useTldrawApp() const theme = app.useStore(themeSelector) const keepOpen = app.useStore(keepOpenSelector) const options = app.useStore(optionsSelector) const currentStyle = app.useStore(currentStyleSelector) const selectedIds = app.useStore(selectedIdsSelector) const [displayedStyle, setDisplayedStyle] = React.useState(currentStyle) const rDisplayedStyle = React.useRef(currentStyle) React.useEffect(() => { const { appState: { currentStyle }, page, selectedIds, } = app let commonStyle = {} as ShapeStyles if (selectedIds.length <= 0) { commonStyle = currentStyle } else { const overrides = new Set([]) app.selectedIds .map((id) => page.shapes[id]) .forEach((shape) => { STYLE_KEYS.forEach((key) => { if (overrides.has(key)) return if (commonStyle[key] === undefined) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore commonStyle[key] = shape.style[key] } else { if (commonStyle[key] === shape.style[key]) return // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore commonStyle[key] = shape.style[key] overrides.add(key) } }) }) } // Until we can work out the correct logic for deciding whether or not to // update the selected style, do a string comparison. Yuck! if (JSON.stringify(commonStyle) !== JSON.stringify(rDisplayedStyle.current)) { rDisplayedStyle.current = commonStyle setDisplayedStyle(commonStyle) } }, [currentStyle, selectedIds]) const handleToggleKeepOpen = React.useCallback((checked: boolean) => { app.setSetting('keepStyleMenuOpen', checked) }, []) const handleToggleFilled = React.useCallback((checked: boolean) => { app.style({ isFilled: checked }) }, []) const handleDashChange = React.useCallback((value: string) => { app.style({ dash: value as DashStyle }) }, []) const handleSizeChange = React.useCallback((value: string) => { 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 }) }, []) const handleMenuOpenChange = React.useCallback( (open: boolean) => { app.setMenuOpen(open) }, [app] ) return ( {displayedStyle.isFilled && ( )} {DASH_ICONS[displayedStyle.dash]} {Object.keys(strokes.light).map((style: string) => ( app.style({ color: style as ColorStyle })} > ))} {Object.values(DashStyle).map((style) => ( {DASH_ICONS[style as DashStyle]} ))} {Object.values(SizeStyle).map((sizeStyle) => ( {SIZE_ICONS[sizeStyle as SizeStyle]} ))} {(options === 'text' || options === 'label') && ( <> {Object.values(FontStyle).map((fontStyle) => ( Aa ))} {options === 'text' && ( {Object.values(AlignStyle).map((style) => ( {ALIGN_ICONS[style]} ))} )} )} ) }) const ColorGrid = styled('div', { display: 'grid', gridTemplateColumns: 'repeat(4, auto)', gap: 0, }) export const StyledRow = styled('div', { position: 'relative', width: '100%', background: 'none', border: 'none', cursor: 'pointer', minHeight: '32px', outline: 'none', color: '$text', fontFamily: '$ui', fontWeight: 400, fontSize: '$1', padding: '$2 0 $2 $3', borderRadius: 4, userSelect: 'none', margin: 0, display: 'flex', gap: '$3', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', variants: { variant: { tall: { alignItems: 'flex-start', padding: '0 0 0 $3', '& > span': { paddingTop: '$4', }, }, }, }, }) const StyledGroup = styled(DropdownMenu.DropdownMenuRadioGroup, { display: 'flex', flexDirection: 'row', gap: '$1', }) const OverlapIcons = styled('div', { display: 'grid', '& > *': { gridColumn: 1, 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', }, }, }, })