Bladeren bron

Feat (Whiteboards): Support text overlay on shapes (#7473)

* 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 <[email protected]>
Konstantinos 3 jaren geleden
bovenliggende
commit
da0f3eb5b4

+ 11 - 12
tldraw/apps/tldraw-logseq/src/components/ContextBar/contextBarActionFactory.tsx

@@ -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>()
 
@@ -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'],
@@ -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
@@ -112,7 +111,7 @@ const EditAction = observer(() => {
         }
       }}
     >
-      <TablerIcon name="text" />
+      <TablerIcon name={iconName} />
     </Button>
   )
 })

+ 5 - 1
tldraw/apps/tldraw-logseq/src/components/GeometryTools/GeometryTools.tsx

@@ -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}>

+ 133 - 45
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'
 
@@ -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>) => {

+ 128 - 35
tldraw/apps/tldraw-logseq/src/lib/shapes/EllipseShape.tsx

@@ -1,14 +1,26 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { TLEllipseShapeProps, TLEllipseShape, getComputedColor } from '@tldraw/core'
+import {
+  TLEllipseShapeProps,
+  TLEllipseShape,
+  getComputedColor,
+  getTextLabelSize,
+} from '@tldraw/core'
 import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import Vec from '@tldraw/vec'
+import * as React from 'react'
 import { observer } from 'mobx-react-lite'
 import { CustomStyleProps, withClampedStyles } from './style-props'
-
+import { TextLabel } from './text/TextLabel'
 export interface EllipseShapeProps extends TLEllipseShapeProps, CustomStyleProps {
   type: 'ellipse'
   size: number[]
+  label: string
+  fontWeight: number
+  italic: boolean
 }
 
+const font = '18px / 1 var(--ls-font-family)'
+
 export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
   static id = 'ellipse'
 
@@ -21,50 +33,131 @@ export class EllipseShape extends TLEllipseShape<EllipseShapeProps> {
     stroke: '',
     fill: '',
     noFill: false,
+    fontWeight: 400,
+    italic: false,
     strokeType: 'line',
     strokeWidth: 2,
     opacity: 1,
+    label: '',
   }
 
-  ReactComponent = observer(({ isSelected, isErasing, events }: TLComponentProps) => {
-    const {
-      size: [w, h],
-      stroke,
-      fill,
-      noFill,
-      strokeWidth,
-      strokeType,
-      opacity,
-    } = this.props
-    return (
-      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
-        <ellipse
-          className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
-          cx={w / 2}
-          cy={h / 2}
-          rx={Math.max(0.01, (w - strokeWidth) / 2)}
-          ry={Math.max(0.01, (h - strokeWidth) / 2)}
-        />
-        <ellipse
-          cx={w / 2}
-          cy={h / 2}
-          rx={Math.max(0.01, (w - strokeWidth) / 2)}
-          ry={Math.max(0.01, (h - strokeWidth) / 2)}
-          strokeWidth={strokeWidth}
-          stroke={getComputedColor(stroke, 'stroke')}
-          strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
-          fill={noFill ? 'none' : getComputedColor(fill, 'background')}
-        />
-      </SVGContainer>
-    )
-  })
+  canEdit = true
+
+  ReactComponent = observer(
+    ({ isSelected, isErasing, events, isEditing, onEditingEnd }: TLComponentProps) => {
+      const {
+        size: [w, h],
+        stroke,
+        fill,
+        noFill,
+        strokeWidth,
+        strokeType,
+        opacity,
+        label,
+        italic,
+        fontWeight,
+      } = this.props
+
+      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}>
+            <ellipse
+              className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
+              cx={w / 2}
+              cy={h / 2}
+              rx={Math.max(0.01, (w - strokeWidth) / 2)}
+              ry={Math.max(0.01, (h - strokeWidth) / 2)}
+            />
+            <ellipse
+              cx={w / 2}
+              cy={h / 2}
+              rx={Math.max(0.01, (w - strokeWidth) / 2)}
+              ry={Math.max(0.01, (h - strokeWidth) / 2)}
+              strokeWidth={strokeWidth}
+              stroke={getComputedColor(stroke, 'stroke')}
+              strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
+              fill={noFill ? 'none' : getComputedColor(fill, 'background')}
+            />
+          </SVGContainer>
+        </div>
+      )
+    }
+  )
 
   ReactIndicator = observer(() => {
     const {
       size: [w, h],
+      label,
+      fontWeight,
     } = this.props
+
+    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(() => {
+      const offset = Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
+      return offset
+    }, [bounds, scale, midPoint])
+
     return (
-      <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} strokeWidth={2} fill="transparent" />
+      <g>
+        <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} strokeWidth={2} 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>
     )
   })
 

+ 11 - 4
tldraw/apps/tldraw-logseq/src/lib/shapes/LineShape.tsx

@@ -14,6 +14,8 @@ import { TextLabel } from './text/TextLabel'
 interface LineShapeProps extends CustomStyleProps, TLLineShapeProps {
   type: 'line'
   label: string
+  fontWeight: number
+  italic: boolean
 }
 
 const font = '18px / 1 var(--ls-font-family)'
@@ -33,6 +35,8 @@ export class LineShape extends TLLineShape<LineShapeProps> {
     stroke: '',
     fill: '',
     noFill: true,
+    fontWeight: 400,
+    italic: false,
     strokeType: 'line',
     strokeWidth: 1,
     opacity: 1,
@@ -51,6 +55,8 @@ export class LineShape extends TLLineShape<LineShapeProps> {
       handles: { start, end },
       opacity,
       label,
+      italic,
+      fontWeight,
       id,
     } = this.props
     const labelSize = label || isEditing ? getTextLabelSize(label, font, 4) : [0, 0]
@@ -62,8 +68,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
     )
     const bounds = this.getBounds()
     const offset = React.useMemo(() => {
-      const offset = Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
-      return offset
+      return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
     }, [bounds, scale, midPoint])
     const handleLabelChange = React.useCallback(
       (label: string) => {
@@ -83,6 +88,9 @@ export class LineShape extends TLLineShape<LineShapeProps> {
           isEditing={isEditing}
           onChange={handleLabelChange}
           onBlur={onEditingEnd}
+          fontStyle={italic ? 'italic' : 'normal'}
+          fontWeight={fontWeight}
+          pointerEvents={!!label}
         />
         <SVGContainer opacity={isErasing ? 0.2 : opacity} id={id + '_svg'}>
           <LabelMask id={id} bounds={bounds} labelSize={labelSize} offset={offset} scale={scale} />
@@ -111,8 +119,7 @@ export class LineShape extends TLLineShape<LineShapeProps> {
       Math.min(1, Math.max(dist / (labelSize[1] + 128), dist / (labelSize[0] + 128)))
     )
     const offset = React.useMemo(() => {
-      const offset = Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
-      return offset
+      return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
     }, [bounds, scale, midPoint])
     return (
       <g>

+ 2 - 2
tldraw/apps/tldraw-logseq/src/lib/shapes/LogseqPortalShape.tsx

@@ -400,7 +400,8 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         if (
           boundScreenCenter[0] > screenSize[0] - 400 ||
           boundScreenCenter[1] > screenSize[1] - 240 ||
-          app.viewport.camera.zoom > 1.5 || app.viewport.camera.zoom < 0.5
+          app.viewport.camera.zoom > 1.5 ||
+          app.viewport.camera.zoom < 0.5
         ) {
           app.viewport.zoomToBounds({ ...this.bounds, minY: this.bounds.maxY + 25 })
         }
@@ -445,7 +446,6 @@ export class LogseqPortalShape extends TLBoxShape<LogseqPortalShapeProps> {
         }}
         {...events}
       >
-        <div className=""></div>
         {isBinding && <BindingIndicator mode="html" strokeWidth={strokeWidth} size={size} />}
         <div
           data-inner-events={!tlEventsEnabled}

+ 143 - 32
tldraw/apps/tldraw-logseq/src/lib/shapes/PolygonShape.tsx

@@ -1,13 +1,26 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
-import { TLPolygonShape, TLPolygonShapeProps, getComputedColor } from '@tldraw/core'
+import {
+  TLPolygonShape,
+  TLPolygonShapeProps,
+  getComputedColor,
+  getTextLabelSize,
+} from '@tldraw/core'
 import { SVGContainer, TLComponentProps } from '@tldraw/react'
+import Vec from '@tldraw/vec'
+import * as React from 'react'
 import { observer } from 'mobx-react-lite'
 import { CustomStyleProps, withClampedStyles } from './style-props'
+import { TextLabel } from './text/TextLabel'
 
 interface PolygonShapeProps extends TLPolygonShapeProps, CustomStyleProps {
   type: 'polygon'
+  label: string
+  fontWeight: number
+  italic: boolean
 }
 
+const font = '18px / 1 var(--ls-font-family)'
+
 export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
   static id = 'polygon'
 
@@ -22,50 +35,148 @@ export class PolygonShape extends TLPolygonShape<PolygonShapeProps> {
     isFlippedY: false,
     stroke: '',
     fill: '',
+    fontWeight: 400,
+    italic: false,
     noFill: false,
     strokeType: 'line',
     strokeWidth: 2,
     opacity: 1,
+    label: '',
   }
 
-  ReactComponent = observer(({ events, isErasing, isSelected }: TLComponentProps) => {
-    const {
-      offset: [x, y],
-      props: { stroke, fill, noFill, strokeWidth, opacity, strokeType },
-    } = this
-    const path = this.getVertices(strokeWidth / 2).join()
-    return (
-      <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
-        <g transform={`translate(${x}, ${y})`}>
-          <polygon
-            className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
-            points={path}
-          />
-          <polygon
-            points={path}
-            stroke={getComputedColor(stroke, 'stroke')}
-            fill={noFill ? 'none' : getComputedColor(fill, 'background')}
-            strokeWidth={strokeWidth}
-            rx={2}
-            ry={2}
-            strokeLinejoin="round"
-            strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
+  canEdit = true
+
+  ReactComponent = observer(
+    ({ events, isErasing, isSelected, isEditing, onEditingEnd }: TLComponentProps) => {
+      const {
+        offset: [x, y],
+        props: {
+          stroke,
+          fill,
+          noFill,
+          strokeWidth,
+          opacity,
+          strokeType,
+          label,
+          italic,
+          fontWeight,
+        },
+      } = this
+
+      const path = this.getVertices(strokeWidth / 2).join()
+
+      const labelSize =
+        label || isEditing
+          ? getTextLabelSize(
+              label,
+              { fontFamily: 'var(--ls-font-family)', fontSize: 18, lineHeight: 1, fontWeight },
+              4
+            )
+          : [0, 0]
+      // Using the centroid of the polygon as the label position is preferable in this case
+      // This shape is an isosceles triangle at the time of writing this comment
+      const midPoint = [this.props.size[0] / 2, (this.props.size[1] * 2) / 3]
+      const scale = Math.max(
+        0.5,
+        Math.min(
+          1,
+          this.props.size[0] / (labelSize[0] * 2),
+          this.props.size[1] / (labelSize[1] * 2)
+        )
+      )
+      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={scale}
+            isEditing={isEditing}
+            onChange={handleLabelChange}
+            onBlur={onEditingEnd}
+            fontStyle={italic ? 'italic' : 'normal'}
+            fontWeight={fontWeight}
           />
-        </g>
-      </SVGContainer>
-    )
-  })
+          <SVGContainer {...events} opacity={isErasing ? 0.2 : opacity}>
+            <g transform={`translate(${x}, ${y})`}>
+              <polygon
+                className={isSelected || !noFill ? 'tl-hitarea-fill' : 'tl-hitarea-stroke'}
+                points={path}
+              />
+              <polygon
+                points={path}
+                stroke={getComputedColor(stroke, 'stroke')}
+                fill={noFill ? 'none' : getComputedColor(fill, 'background')}
+                strokeWidth={strokeWidth}
+                rx={2}
+                ry={2}
+                strokeLinejoin="round"
+                strokeDasharray={strokeType === 'dashed' ? '8 2' : undefined}
+              />
+            </g>
+          </SVGContainer>
+        </div>
+      )
+    }
+  )
 
   ReactIndicator = observer(() => {
     const {
       offset: [x, y],
-      props: { strokeWidth },
+      props: { label, strokeWidth, fontWeight },
     } = this
+
+    const bounds = this.getBounds()
+    const labelSize = label
+      ? getTextLabelSize(
+          label,
+          { fontFamily: 'var(--ls-font-family)', fontSize: 18, lineHeight: 1, fontWeight },
+          4
+        )
+      : [0, 0]
+    const midPoint = [this.props.size[0] / 2, (this.props.size[1] * 2) / 3]
+    const scale = Math.max(
+      0.5,
+      Math.min(1, this.props.size[0] / (labelSize[0] * 2), this.props.size[1] / (labelSize[1] * 2))
+    )
+
+    const offset = React.useMemo(() => {
+      return Vec.sub(midPoint, Vec.toFixed([bounds.width / 2, bounds.height / 2]))
+    }, [bounds, scale, midPoint])
+
     return (
-      <polygon
-        transform={`translate(${x}, ${y})`}
-        points={this.getVertices(strokeWidth / 2).join()}
-      />
+      <g>
+        <polygon
+          transform={`translate(${x}, ${y})`}
+          points={this.getVertices(strokeWidth / 2).join()}
+        />
+        {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>
     )
   })
 

+ 17 - 3
tldraw/apps/tldraw-logseq/src/lib/shapes/text/TextLabel.tsx

@@ -9,22 +9,28 @@ export interface TextLabelProps {
   font: string
   text: string
   color: string
+  fontStyle: string
+  fontWeight: number
   onBlur?: () => void
   onChange: (text: string) => void
   offsetY?: number
   offsetX?: number
   scale?: number
   isEditing?: boolean
+  pointerEvents?: boolean
 }
 
 export const TextLabel = React.memo(function TextLabel({
   font,
   text,
   color,
+  fontStyle,
+  fontWeight,
   offsetX = 0,
   offsetY = 0,
   scale = 1,
   isEditing = false,
+  pointerEvents = false,
   onBlur,
   onChange,
 }: TextLabelProps) {
@@ -121,11 +127,15 @@ export const TextLabel = React.memo(function TextLabel({
   React.useLayoutEffect(() => {
     const elm = rInnerWrapper.current
     if (!elm) return
-    const size = getTextLabelSize(text, font, 4)
+    const size = getTextLabelSize(
+      text,
+      { fontFamily: 'var(--ls-font-family)', fontSize: 18, lineHeight: 1, fontWeight },
+      4
+    )
     elm.style.transform = `scale(${scale}, ${scale}) translate(${offsetX}px, ${offsetY}px)`
     elm.style.width = size[0] + 1 + 'px'
     elm.style.height = size[1] + 1 + 'px'
-  }, [text, font, offsetY, offsetX, scale])
+  }, [text, fontWeight, offsetY, offsetX, scale])
 
   return (
     <div className="tl-text-label-wrapper">
@@ -134,8 +144,10 @@ export const TextLabel = React.memo(function TextLabel({
         ref={rInnerWrapper}
         style={{
           font,
+          fontStyle,
+          fontWeight,
           color,
-          pointerEvents: text ? 'all' : 'none',
+          pointerEvents: pointerEvents ? 'all' : 'none',
           userSelect: isEditing ? 'text' : 'none',
         }}
       >
@@ -145,6 +157,8 @@ export const TextLabel = React.memo(function TextLabel({
             style={{
               font,
               color,
+              fontStyle,
+              fontWeight,
             }}
             className="tl-text-label-textarea"
             name="text"

+ 7 - 4
tldraw/apps/tldraw-logseq/src/styles.css

@@ -339,7 +339,6 @@ button.tl-select-input-trigger {
   &[aria-expanded='true'] {
     .tl-popover-indicator {
       transform: rotate(180deg);
-      color: #000;
     }
   }
 }
@@ -351,6 +350,10 @@ button.tl-select-input-trigger {
   pointer-events: none;
   left: 0;
   bottom: -3px;
+
+  &[data-selected='true'] {
+    color: #000;
+  }
 }
 
 .floating-panel[data-tool-locked='true'] > .tl-button[data-selected='true']::after {
@@ -427,7 +430,7 @@ button.tl-select-input-trigger {
 
 .tl-text-label-inner-wrapper {
   position: absolute;
-  padding: 4px;
+  padding: 6px 4px 4px;
   z-index: 1;
   min-height: 1px;
   min-width: 1px;
@@ -446,7 +449,7 @@ button.tl-select-input-trigger {
 
   z-index: 1;
   border: none;
-  padding: 4px;
+  padding: 6px 4px 4px;
   resize: none;
   text-align: inherit;
   min-height: inherit;
@@ -459,7 +462,7 @@ button.tl-select-input-trigger {
   backface-visibility: hidden;
   display: inline-block;
   pointer-events: all;
-  background: transparent;
+  background: transparent !important;
   user-select: text;
   -webkit-font-smoothing: subpixel-antialiased;
   white-space: pre-wrap;