Arrange tool, and a hold that is easier to find

An Arrange tool marks the corners of every image, Hide and own text
rectangle in blue and moves or resizes them by a plain drag, a press
elsewhere doing nothing.  The hold that does the same with any tool is
shorter (450 ms), with more slack, and shows all four corners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-10-14 00:01:45 +02:00
1 parent 94a3c1ba0e
commit 8ecf3e94a6
6 files changed
+260 -25

No files matched your search

+40 -5
View File
@@ -460,7 +460,7 @@ async function enterSession(invitationText, name, notebook) {
showInvitationCode(config.site ? input('invitation').value : '');
const isHost = identity.id === invitation.hostId;
let tool = /** @type {'pen' | 'eraser' | 'text' | 'hide'} */ ('pen');
let tool = /** @type {'pen' | 'eraser' | 'text' | 'hide' | 'arrange'} */ ('pen');
let color = DEFAULT_COLOR;
let width = WIDTHS[1];
let textSize = TEXT_SIZE;
@@ -510,6 +510,7 @@ async function enterSession(invitationText, name, notebook) {
onTextChanged: (layerId) => {
syncLayers();
renderer.redrawLayer(layerId);
if (layerId === session.identity.id) refreshArrangeCues();
writer.changed();
},
onPageReplaced: (oldPage, newPage) => {
@@ -529,11 +530,13 @@ async function enterSession(invitationText, name, notebook) {
syncLayers();
showParticipants(session, renderer);
showPageNumber();
refreshArrangeCues();
},
onImagesChanged: () => {
log(`Page ${session.page.number}: ${session.page.images.length} images, ${session.page.hides.length} hides`);
renderer.showImages();
renderer.drawHides();
refreshArrangeCues();
writer.writeNow();
},
onPageResized: () => {
@@ -552,6 +555,7 @@ async function enterSession(invitationText, name, notebook) {
(session.isHost ? `; guests allowed: ${session.permitted.size}` : ''));
showHostTools();
showParticipants(session, renderer);
refreshArrangeCues();
},
onParticipantsChanged: () => {
syncLayers();
@@ -781,6 +785,32 @@ async function enterSession(invitationText, name, notebook) {
return block ? textBounds(block) : null;
}
/**
* With the Arrange tool chosen, mark what a press can move or resize:
* the images and Hides (for the host and co-hosts) and one's own text
* rectangles; free text can be moved but has no corners. Called
* whenever any of them, or the right, may have changed.
*/
function refreshArrangeCues() {
if (tool !== 'arrange') {
renderer.setArrangeCues([]);
return;
}
const page = session.page;
/** @type {{rect: import('./page_model.js').Rectangle, kind: 'image' | 'text' | 'hide'}[]} */
const cues = [];
if (session.mayMakePages()) {
for (const im of page.images) cues.push({ kind: 'image', rect: { left: im.x, top: im.y, width: im.width, height: im.height } });
}
for (const block of session.ownLayer.texts.values()) {
if (block.height !== null) cues.push({ kind: 'text', rect: textBounds(block) });
}
if (session.mayMakePages()) {
for (const h of page.hides) cues.push({ kind: 'hide', rect: { left: h.x, top: h.y, width: h.width, height: h.height } });
}
renderer.setArrangeCues(cues);
}
/** @type {import('./pen_input.js').PenTarget} */
const penTarget = {
toPage: (clientX, clientY) => renderer.pageFromClient(clientX, clientY),
@@ -871,6 +901,7 @@ async function enterSession(invitationText, name, notebook) {
if (textEditor?.block?.id === block.id) textEditor.layout();
}
renderer.showGrip(currentHeldRect(held), held.corner);
refreshArrangeCues();
},
holdEnd: () => {
if (!held) return;
@@ -881,6 +912,7 @@ async function enterSession(invitationText, name, notebook) {
if (held.kind === 'hide') session.endHideEdit();
held = null;
renderer.showGrip(null);
refreshArrangeCues();
},
eraseAlong: (x0, y0, x1, y1) => {
textEditor?.stop();
@@ -979,10 +1011,12 @@ async function enterSession(invitationText, name, notebook) {
if (value !== tool) trace(`Tool: ${value}`);
tool = value;
// Only the chosen tool's settings show: the pen's colours and line
// widths, the text's colours, size and font, nothing for the eraser.
element('colors').hidden = value === 'eraser' || value === 'hide';
// widths, the text's colours, size and font, nothing for the
// eraser, Hide or Arrange.
element('colors').hidden = value === 'eraser' || value === 'hide' || value === 'arrange';
element('widths').hidden = value !== 'pen';
element('text_style').hidden = value !== 'text';
refreshArrangeCues();
},
setColor: (value) => {
color = value;
@@ -1249,7 +1283,7 @@ function press(group, chosen) {
* @param {number} options.width Initial width.
* @param {number} options.textSize
* @param {import('./page_model.js').TextFont} options.textFont
* @param {(tool: 'pen' | 'eraser' | 'text' | 'hide') => void} options.setTool
* @param {(tool: 'pen' | 'eraser' | 'text' | 'hide' | 'arrange') => void} options.setTool
* @param {(color: string) => void} options.setColor
* @param {(width: number) => void} options.setWidth
* @param {(size: number) => void} options.setTextSize
@@ -1265,7 +1299,8 @@ function buildToolbar(options) {
other.setAttribute('aria-pressed', String(other === button));
}
const chosen = button.getAttribute('data-tool');
options.setTool(chosen === 'eraser' ? 'eraser' : chosen === 'text' ? 'text' : chosen === 'hide' ? 'hide' : 'pen');
options.setTool(chosen === 'eraser' ? 'eraser' : chosen === 'text' ? 'text' : chosen === 'hide' ? 'hide'
: chosen === 'arrange' ? 'arrange' : 'pen');
});
}