The Shape tool, implied rectangles, and the palette floating again

A Shapes button right of Text draws a rectangle, an ellipse, a line of
taps or an arrow in the pen's colour and width, with a fill colour of
its own; a hold, a secondary press or two quick clicks open its menu.
Text and Hide rectangles draw nothing on the page and show blue corners
while the pointer hovers over them; free text's red corner mark is
gone.  Arrange moves and resizes shapes.  The colour palette floats
below "+" again instead of reflowing the toolbar.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 14:00:47 +02:00
1 parent 46a82bf3b7
commit 03907da8c9
12 files changed
+1502 -297

No files matched your search

+7 -24
View File
@@ -12,7 +12,6 @@
import { codeAtEnd } from './character_codes.js';
import { randomId } from './crypto.js';
import { textBounds } from './page_model.js';
import { TEXT_FRAME_WIDTH, TEXT_MARK } from './page_file.js';
import { HOLD_MS, HOLD_SLACK_PX } from './pen_input.js';
import { LEFT_GAP, LINE_HEIGHT, TOP_GAP, baselineOf, fontMetrics, layoutText, trimTrailingBlankLines } from './text_layout.js';
@@ -67,15 +66,14 @@ export class TextEditor {
}
});
/** Free text's corner mark while it is being edited (the renderer leaves the block out then). */
this.mark = document.createElement('div');
this.mark.className = 'text_mark';
this.mark.hidden = true;
// The block's marks (free text's red L, a rectangle's blue corners)
// are drawn by the renderer while it is edited as at any other time
// (since 2026-10-14; the editor showed its own before).
/** Shows a rectangle while it is being dragged open. */
this.preview = document.createElement('div');
this.preview.className = 'rect_preview';
this.preview.hidden = true;
this.pageElement.append(this.field, this.mark, this.preview);
this.pageElement.append(this.field, this.preview);
}
/**
@@ -216,7 +214,6 @@ export class TextEditor {
this.block = null;
this.renderer.editingText = null;
this.field.hidden = true;
this.mark.hidden = true;
this.field.value = '';
trimTrailingBlankLines(block);
if (block.text.trim() === '') this.target.removeText(block.id);
@@ -237,7 +234,6 @@ export class TextEditor {
this.block = null;
this.renderer.editingText = null;
this.field.hidden = true;
this.mark.hidden = true;
this.field.value = '';
return block;
}
@@ -306,12 +302,6 @@ export class TextEditor {
const { ascent, descent } = fontMetrics(block.size, block.font);
// In a line box the baseline lies half the leading and the ascent down.
const baselineInLine = (lineHeight - (ascent + descent) * scale) / 2 + ascent * scale;
// A box's field is exactly the box, on whole pixels, and draws the
// frame as an outline inside its edge, which takes no room, at the
// width the canvas draws it.
const frame = Math.max(1, Math.round(TEXT_FRAME_WIDTH * scale * dpr)) / dpr;
style.outlineWidth = isRect ? `${frame}px` : '0';
style.outlineOffset = `${-frame}px`;
const padTop = firstBaseline - top - baselineInLine;
style.paddingTop = `${padTop}px`;
style.paddingLeft = `${textLeft - left}px`;
@@ -324,13 +314,12 @@ export class TextEditor {
style.color = block.color;
style.whiteSpace = block.width === null ? 'pre' : 'pre-wrap';
if (isRect) {
// A box's field is exactly the box, on whole pixels.
const bounds = textBounds(block);
// The canvas strokes the far lines on the pixel at the edge itself,
// so the field reaches one pixel past it.
style.boxSizing = 'border-box';
style.width = `${whole((bounds.left + bounds.width - page.left) * scale) - left + 1 / dpr}px`;
style.width = `${whole((bounds.left + bounds.width - page.left) * scale) - left}px`;
style.height = `${Math.max(block.lines.length * lineHeight + padTop + slack,
whole((bounds.top + bounds.height - page.top) * scale) - top + 1 / dpr)}px`;
whole((bounds.top + bounds.height - page.top) * scale) - top)}px`;
} else {
style.boxSizing = 'content-box';
// A little wider than the block, so that the field never wraps a
@@ -339,11 +328,5 @@ export class TextEditor {
style.height = `${block.lines.length * lineHeight}px`;
}
this.field.scrollTop = 0;
this.mark.hidden = isRect;
this.mark.style.left = `${left}px`;
this.mark.style.top = `${top}px`;
this.mark.style.width = `${whole(TEXT_MARK * scale)}px`;
this.mark.style.height = `${whole(TEXT_MARK * scale)}px`;
this.mark.style.borderWidth = `${Math.max(1, Math.round(TEXT_FRAME_WIDTH * scale * dpr)) / dpr}px`;
}
}