fix: show the caret in new, empty text (#932)
CanvasKit lays out no line for an empty paragraph, so the text editor found no caret until the first character was typed. A one-space line now gives an empty text's caret its height, and its alignment places it.
This commit is contained in:
parent
b0e0b321e9
commit
08c13dabbd
|
|
@ -93,6 +93,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Show the blinking caret in a new, empty text layer before the first character is typed.
|
||||
- Text saved to `.fig` or copied to Figma keeps its layout in Figma: it wraps at the layer's width with its alignment and line height, and keeps ligatures and contextual forms such as Inter's arrows, where Figma previously drew every saved OpenPencil text layer on one unwrapped line (#914).
|
||||
- Release a document's memory when its tab closes. Every closed tab kept its scene, canvas, and editor panels alive until reload, so memory grew with each document opened and closed. Menus and shortcuts now also follow the active document, so Undo and Redo are offered according to its history rather than the first document opened.
|
||||
- Start Pi chats with OpenPencil's MCP tools when the Harness companion runs on Node 22.15 or later; the companion now installs the dependency Pi's MCP adapter needs and loads its TypeScript sources. A reopened Pi session starts fresh instead of failing to resume its in-memory sandbox, and npm output from Pi no longer mixes into the companion's protocol.
|
||||
|
|
|
|||
|
|
@ -340,6 +340,31 @@ export class TextEditor {
|
|||
this.moveWord(extend, 'right')
|
||||
}
|
||||
|
||||
/**
|
||||
* The caret of empty text. CanvasKit lays out no line for an empty paragraph, so a line
|
||||
* holding one space gives the caret's height, and the alignment its place.
|
||||
*/
|
||||
private emptyCaret(paragraph: Paragraph): TextCaret | null {
|
||||
const node = this.paragraphNode
|
||||
const line = paragraph.getLineMetrics().at(0)
|
||||
if (line) {
|
||||
const offsetY = this.paragraphVerticalOffset()
|
||||
return { x: line.left, y0: offsetY, y1: offsetY + line.height }
|
||||
}
|
||||
if (!node || !this.renderer) return null
|
||||
const probe = this.renderer.buildParagraph({ ...node, text: ' ' })
|
||||
const height = probe.getLineMetrics()[0]?.height ?? probe.getHeight()
|
||||
probe.delete()
|
||||
const available = Math.max(0, node.height - height)
|
||||
let offsetY = 0
|
||||
if (node.textAlignVertical === 'CENTER') offsetY = available / 2
|
||||
if (node.textAlignVertical === 'BOTTOM') offsetY = available
|
||||
let x = 0
|
||||
if (node.textAlignHorizontal === 'CENTER') x = node.width / 2
|
||||
if (node.textAlignHorizontal === 'RIGHT') x = node.width
|
||||
return { x, y0: offsetY, y1: offsetY + height }
|
||||
}
|
||||
|
||||
getCaretRect(): TextCaret | null {
|
||||
const s = this._state
|
||||
if (!s?.paragraph) return null
|
||||
|
|
@ -347,13 +372,7 @@ export class TextEditor {
|
|||
const text = s.text
|
||||
const cursor = s.cursor
|
||||
|
||||
if (text.length === 0) {
|
||||
const metrics = s.paragraph.getLineMetrics()
|
||||
if (metrics.length === 0) return null
|
||||
const line = metrics[0]
|
||||
const offsetY = this.paragraphVerticalOffset()
|
||||
return { x: line.left, y0: offsetY, y1: offsetY + line.height }
|
||||
}
|
||||
if (text.length === 0) return this.emptyCaret(s.paragraph)
|
||||
|
||||
let lo: number
|
||||
let hi: number
|
||||
|
|
|
|||
|
|
@ -189,6 +189,36 @@ describe('TextEditor', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('places the caret of empty text, which CanvasKit lays out no line for', () => {
|
||||
const empty = asDouble<Paragraph>({
|
||||
delete: () => undefined,
|
||||
getHeight: () => 0,
|
||||
getLineMetrics: () => []
|
||||
})
|
||||
const space = asDouble<Paragraph>({
|
||||
delete: () => undefined,
|
||||
getHeight: () => 17,
|
||||
getLineMetrics: () => [{ height: 17, left: 0 }]
|
||||
})
|
||||
const editor = new TextEditor(mockCk)
|
||||
editor.setRenderer(
|
||||
asDouble<Parameters<TextEditor['setRenderer']>[0]>({
|
||||
buildParagraph: (node: SceneNode) => (node.text === '' ? empty : space),
|
||||
fontGeneration: 1
|
||||
})
|
||||
)
|
||||
editor.start(
|
||||
createDefaultNode(() => 'new-text', 'TEXT', {
|
||||
text: '',
|
||||
width: 120,
|
||||
height: 57,
|
||||
textAlignHorizontal: 'CENTER',
|
||||
textAlignVertical: 'BOTTOM'
|
||||
})
|
||||
)
|
||||
expect(editor.getCaretRect()).toEqual({ x: 60, y0: 40, y1: 57 })
|
||||
})
|
||||
|
||||
test('offsets the empty-text caret for vertical alignment', () => {
|
||||
const { editor } = createParagraphEditor('BOTTOM')
|
||||
editorState(editor).text = ''
|
||||
|
|
|
|||
Loading…
Reference in a new issue