Lists add and delete items right in the list header and rows

The layer, frame and shape lists still had their add and delete
buttons scattered below the list, each in its own layout. They now
live in the shared SortableList, so every list works the same way.

- SortableList gets an optional header with the title on the left
  and a + on the right, plus an optional x per row next to the eye.
- The keepOne option hides the x on the last remaining row, so a
  pixel art image and a tilemap always keep one layer.
- Pixel art layers and frames and tilemap layers use header, + and
  x. Only Merge down and Duplicate stay below the lists.
- Vector art shapes get the x but no +. Add shape stays below the
  list and Delete shapes stays for a multi-selection.
- The section headings in the pixel art column use the same style
  as the list titles and the inspector.

Findings:

- The wait loop added to check 47i for #196 only waited for the
  right half of the shape. If the texture landed between the two
  reads, the left half was still blue and the check failed. It now
  waits for both halves.

Tests:

- ./check.sh: 138 files, 2052 tests, exit 0.
- GM_STEPS=39,40,47,48 (fresh): ALL PASSED, 156 checks (was 154).
- GM_STEPS=47,48 (fresh, final): ALL PASSED, 86 checks.
- Two new e2e checks: x on a shape (47) and x on a frame (48). The
  tilemap check in 40 now uses the x instead of Delete layer.
- Counter-check: with the x not reporting to its owner, exactly the
  two new checks fail.
- Check total 1266 in README and wiki is computed (1264 + 2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-21 21:18:26 +02:00
1 parent 5b9b57fd19
commit ae0247d86f
11 files changed
+264 -100

No files matched your search

+28 -9
View File
@@ -32,7 +32,7 @@ import { currentFontPicker } from './font/FontPicker';
import { currentNodePalette } from './graph/NodePalette';
import { currentSoundPicker } from './sound/SoundPicker';
import { currentImagePicker } from './texture/ImagePicker';
import { currentTilemapWidgets } from './panels/TilemapPanel';
import { currentTilemapWidgets, paintedLayerId, paintedTilemap } from './panels/TilemapPanel';
import { currentResizeDialog } from './pixelart/ResizeCanvasDialog';
import { currentGradientBar, currentShapeList } from './inspector/shapeSection';
import type { SnapToggle } from './graph/SnapToggle';
@@ -514,6 +514,13 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
...Object.fromEntries(Object.entries(side.layerButtons).map(([k, b]) => [`layer:${k}`, b])),
...Object.fromEntries(Object.entries(side.frameButtons).map(([k, b]) => [`frame:${k}`, b])),
};
// The plus in a list's header is no widget of its own.
const list = name === 'layer:add' ? side.layers : name === 'frame:add' ? side.frames : null;
if (list) {
side.reveal(list);
const at = list.addGlobal();
return at ? { ...at, enabled: true, text: '+' } : null;
}
const widget = byName[name];
if (!widget || !widget.visible) return null;
if (widget !== editor.pixelArtEditor.resizeButton) side.reveal(widget);
@@ -530,7 +537,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
},
/**
* The centre of a layer's or frame's row, scrolled into view, with the centre of its picture;
* `eye` is the layer's switch.
* `eye` is the layer's switch, `remove` the cross (null on the last one).
*/
layerRow: (layerId: string) => {
const side = editor.pixelArtEditor.side;
@@ -542,6 +549,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
y: row.y + row.h / 2,
thumb: side.layers.thumbGlobal(layerId),
eye: side.layers.eyeGlobal(layerId),
remove: side.layers.removeGlobal(layerId),
}
: null;
},
@@ -550,7 +558,12 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
side.reveal(side.frames);
const row = side.frames.rowGlobal(frameId);
return row
? { x: row.x + row.w / 2, y: row.y + row.h / 2, thumb: side.frames.thumbGlobal(frameId) }
? {
x: row.x + row.w / 2,
y: row.y + row.h / 2,
thumb: side.frames.thumbGlobal(frameId),
remove: side.frames.removeGlobal(frameId),
}
: null;
},
/** The ids as the two lists show them, top first. */
@@ -647,6 +660,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
*/
shapeRows: () => currentShapeList()?.shownIds ?? null,
shapeRowAt: (shapeId: string) => currentShapeList()?.rowGlobal(shapeId) ?? null,
/** The cross on a shape's row. */
shapeRemoveAt: (shapeId: string) => currentShapeList()?.removeGlobal(shapeId) ?? null,
/** The Paint tool (#186): its button, the "Handles" menu, and which part it shows. */
paintButton: () => centreOf(vectorArtEditor.paintButton),
paintTarget: () => ({
@@ -767,10 +782,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
const swatch = palette?.swatchGlobal(assetId);
return palette && swatch ? { ...swatch, columns: palette.columns } : null;
},
addLayerAt: () => {
const button = currentTilemapWidgets()?.addLayer;
return button ? centreOf(button) : null;
},
/** The plus in the layer list's header. */
addLayerAt: () => currentTilemapWidgets()?.layers.addGlobal() ?? null,
/** The name field of the painted layer and the Show dropdown, with what they hold (#138). */
layerNameAt: () => {
const field = currentTilemapWidgets()?.layerName;
@@ -795,9 +808,15 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
// `y` is the row's own line, so a check can tell whether it is inside the panel.
return { id, y: name.y, name, visible: list.eyeGlobal(id) };
},
/** The cross on the painted layer's row; `enabled` is false where the row has none (the last layer). */
removeLayerAt: () => {
const button = currentTilemapWidgets()?.removeLayer;
return button ? { ...centreOf(button), enabled: button.enabled } : null;
const widgets = currentTilemapWidgets();
const target = paintedTilemap(state);
if (!widgets || !target) return null;
const layerId = paintedLayerId(state, target.asset);
const at = widgets.layers.removeGlobal(layerId);
const row = widgets.layers.rowGlobal(layerId);
return at ? { ...at, enabled: true } : row ? { x: row.x, y: row.y, enabled: false } : null;
},
/** Where a cell sits on screen – for a click that paints it or a pixel that proves it. */
cellAt: (objectId: string, x: number, y: number) => viewport.cellScreen(objectId, x, y),