import { clickDocumentTab } from './lib/scaffold.mjs'; /** * Tabs as open documents (#171): a double-click in the asset panel opens a tab per asset, several of * one kind at once; the tab keeps its document whatever is selected, and its zoom when another tab * was on screen; the close cross and a middle click close it, a rename renames it and a deleted * asset takes its tab along. */ export async function documents(t) { const { check, json, evaluate, mutate, click, doubleClick, wheel, shot, showTab } = t; await showTab('scene'); for (const doc of (await json('__gm.documents()')).docs) await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`); // Short names of their own: an earlier step leaves them too long for a tab to show whole. const [a, b] = await json(`(() => { const two = __gm.state.project.assets.filter((x) => x.type === 'texture' && x.width > 0).slice(0, 2); const out = two.map((x, i) => ({ id: x.id, name: 'Sheet ' + 'AB'[i], was: x.name })); two.forEach((x, i) => (x.name = out[i].name)); __gm.state.preview(); return out; })()`); const behavior = await json( `(() => { const x = __gm.state.project.assets.find((x) => x.type === 'behavior' && x.graph.nodes.length > 0); return { id: x.id, node: x.graph.nodes[0].id }; })()`, ); const docs = () => json('__gm.documents()'); const current = () => json('__gm.texture.current()'); const place = async (doc) => (await json('__gm.tabRow()')).shown.find((p) => p.tab === doc); // 44a. Two textures, two tabs; each names its kind in front of the name. for (const texture of [a, b]) { const row = await json(`__gm.assetRow(${JSON.stringify(texture.id)})`); await doubleClick(row.x, row.y); } const opened = await docs(); check( 'double-clicking two textures opens a tab for each, the second one showing', JSON.stringify(opened.docs) === JSON.stringify([`asset:${a.id}`, `asset:${b.id}`]) && opened.active === `asset:${b.id}` && (await current()) === b.id, JSON.stringify({ opened, current: await current() }), ); const placeA = await place(`asset:${a.id}`); const inTab = (p) => p.x > placeA.x && p.x < placeA.right && p.y > placeA.y && p.y < placeA.y + placeA.h; const nameHits = await json(`__gm.locateAll(${JSON.stringify(a.name)})`); const kindWords = (await json(`__gm.locateAll('Texture')`)).filter(inTab); // The icon's 16 px in front of the name, read against the tab's own background at its left edge. const iconInk = await json(`(() => { const x0 = ${placeA.x} + 6, y = ${placeA.y + placeA.h / 2}; const grid = []; for (let dy = -7; dy <= 7; dy++) for (let dx = 0; dx < 16; dx++) grid.push({ x: x0 + dx, y: y + dy }); const [background, ...ink] = __gm.pixels([{ x: ${placeA.x} + 1, y }, ...grid]); return ink.filter((p) => p !== background).length; })()`); check( 'a tab shows the icon of its kind, then the asset’s name – the kind no longer as a word', placeA?.kind === 'Texture' && nameHits.some(inTab) && kindWords.length === 0 && iconInk >= 20, JSON.stringify({ placeA, nameHits, kindWords, iconInk }), ); // 44b. Selecting the other texture is a look at the inspector, not a change of document. await evaluate(`__gm.state.select({ kind: 'asset', assetId: ${JSON.stringify(a.id)} })`); check('selecting the other texture leaves the tab on its own', (await current()) === b.id, await current()); // 44c. Zoom and scroll stay with the document. const view = () => json( `(() => { const o = __gm.texture.pointAt(0, 0); const p = __gm.texture.pointAt(10, 0); return { x: o.x, y: o.y, scale: (p.x - o.x) / 10 }; })()`, ); const fitted = await view(); const rect = await json('__gm.texture.rect()'); await wheel(rect.x + rect.w / 2, rect.y + rect.h / 2, -300); const zoomed = await view(); check('the wheel zooms the texture', zoomed.scale > fitted.scale * 1.2, JSON.stringify({ fitted, zoomed })); await clickDocumentTab(t, `asset:${a.id}`); check('its neighbour’s tab shows the other texture', (await current()) === a.id, await current()); await clickDocumentTab(t, `asset:${b.id}`); const back = await view(); check( 'coming back, the texture is zoomed and scrolled as it was left', (await current()) === b.id && Math.abs(back.scale - zoomed.scale) < 1e-6 && Math.abs(back.x - zoomed.x) < 0.5 && Math.abs(back.y - zoomed.y) < 0.5, JSON.stringify({ zoomed, back }), ); // 44d. A rename renames the tab. await mutate( `__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = 'Renamed sheet'; __gm.state.preview();`, ); const renamedAt = await place(`asset:${b.id}`); const renamedHits = await json(`__gm.locateAll('Renamed sheet')`); check( 'renaming the asset renames its tab', renamedHits.some( (p) => p.x > renamedAt.x && p.x < renamedAt.right && p.y > renamedAt.y && p.y < renamedAt.y + renamedAt.h, ), JSON.stringify({ renamedAt, renamedHits }), ); await mutate( `__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(b.id)}).name = ${JSON.stringify(b.name)}; __gm.state.preview();`, ); // 44e. A node lives only in its graph: selecting one brings that graph up, in a tab of its own. await evaluate( `__gm.state.select({ kind: 'node', assetId: ${JSON.stringify(behavior.id)}, nodeId: ${JSON.stringify(behavior.node)} })`, ); const withGraph = await docs(); check( 'selecting a node opens its behavior next to the texture showing', withGraph.active === `asset:${behavior.id}` && JSON.stringify(withGraph.docs) === JSON.stringify([`asset:${a.id}`, `asset:${b.id}`, `asset:${behavior.id}`]) && (await json('__gm.tab()')) === 'graph', JSON.stringify(withGraph), ); await shot('44-documents'); // 44f. The close cross closes the tab on screen; its left neighbour comes up. const graphTab = await place(`asset:${behavior.id}`); await click(graphTab.close.x, graphTab.close.y); const afterClose = await docs(); check( 'the close cross closes the tab, and the one left of it shows', !afterClose.docs.includes(`asset:${behavior.id}`) && afterClose.active === `asset:${b.id}` && (await current()) === b.id, JSON.stringify(afterClose), ); // 44g. A middle click closes a tab that is not showing, and leaves the one that is. const tabA = await place(`asset:${a.id}`); await click((tabA.x + tabA.close.x) / 2, tabA.y + tabA.h / 2, 'middle'); const afterMiddle = await docs(); check( 'a middle click closes a tab in the background', JSON.stringify(afterMiddle.docs) === JSON.stringify([`asset:${b.id}`]) && afterMiddle.active === `asset:${b.id}`, JSON.stringify(afterMiddle), ); // 44h. A deleted asset takes its tab along, and the last one gone leaves the Scene tab showing. await mutate(` const p = __gm.state.project; window.__deletedSheet = p.assets.find((x) => x.id === ${JSON.stringify(b.id)}); p.assets = p.assets.filter((x) => x.id !== ${JSON.stringify(b.id)}); __gm.state.preview(); `); const afterDelete = await docs(); check( 'deleting the asset closes its tab and goes back to the scene', afterDelete.docs.length === 0 && (await json('__gm.tab()')) === 'scene', JSON.stringify({ afterDelete, tab: await json('__gm.tab()') }), ); await mutate( `__gm.state.project.assets.push(window.__deletedSheet); delete window.__deletedSheet; __gm.state.preview();`, ); // 44i. A right-click on a tab floats it: the document leaves the row for a window of its own. await evaluate(`__gm.openDocument(${JSON.stringify(`asset:${a.id}`)})`); const tabOfA = await place(`asset:${a.id}`); await click(tabOfA.x + 20, tabOfA.y + tabOfA.h / 2, 'right'); const floatEntry = (await json(`__gm.locateAll('Float')`)).find( (p) => p.y > tabOfA.y + tabOfA.h && Math.abs(p.x - tabOfA.x) < 200, ); if (floatEntry) await click(floatEntry.x, floatEntry.y); const windows = () => json('__gm.floatWindows()'); const floated = await windows(); const inWindow = await json('__gm.texture.rect()'); const box = floated[0]?.box; check( 'Float in a tab’s menu moves the document into a window, the texture in it', !(await docs()).docs.includes(`asset:${a.id}`) && floated.length === 1 && floated[0].kind === 'texture' && floated[0].doc === `asset:${a.id}` && floated[0].title === a.name && (await json('__gm.tab()')) === 'scene' && (await current()) === a.id && inWindow.x >= box.x && inWindow.x + inWindow.w <= box.x + box.w && inWindow.y > box.y && inWindow.y + inWindow.h <= box.y + box.h, JSON.stringify({ floatEntry, floated, inWindow, docs: await docs() }), ); // 44j. The title bar moves the window, its corner sizes it, and the texture grows along. await t.drag(floated[0].bar.x, floated[0].bar.y, floated[0].bar.x - 100, floated[0].bar.y - 60); const moved = (await windows())[0]; await t.drag(moved.grip.x, moved.grip.y, moved.grip.x + 80, moved.grip.y + 40); const grown = (await windows())[0]; const grownRect = await json('__gm.texture.rect()'); check( 'dragging the title bar moves the window, dragging its corner sizes it', moved.box.x === box.x - 100 && moved.box.y === box.y - 60 && grown.box.w === box.w + 80 && grown.box.h === box.h + 40 && grown.box.x === moved.box.x && grownRect.w === inWindow.w + 80, JSON.stringify({ box, moved: moved.box, grown: grown.box, inWindow, grownRect }), ); await shot('44-float'); // 44k. Another texture opened goes into the window: the kind has one editor, so one window. const rowB = await json(`__gm.assetRow(${JSON.stringify(b.id)})`); await doubleClick(rowB.x, rowB.y); const other = await windows(); check( 'opening another texture shows it in the window, not in a tab', other.length === 1 && other[0].doc === `asset:${b.id}` && other[0].title === b.name && !(await docs()).docs.includes(`asset:${b.id}`) && (await current()) === b.id, JSON.stringify({ other, docs: await docs() }), ); // 44l. Delete, Copy and Paste go where the last press went. await click(other[0].bar.x, other[0].bar.y); const toWindow = await json('__gm.keyTarget()'); const scene = (await json('__gm.tabRow()')).shown.find((p) => p.tab === 'scene'); await click((scene.x + scene.right) / 2, scene.y + scene.h / 2); const toTabs = await json('__gm.keyTarget()'); check( 'keys go to the window after a press in it, and to the tab after a press in the editor', toWindow === 'texture' && toTabs === 'scene', JSON.stringify({ toWindow, toTabs }), ); // 44m. Dock puts the document back into the row, showing, with its pane below the row. await click(other[0].dock.x, other[0].dock.y); const docked = await docs(); const dockedRect = await json('__gm.texture.rect()'); check( 'Dock brings the document back as a tab, showing', (await windows()).length === 0 && docked.active === `asset:${b.id}` && (await json('__gm.tab()')) === 'texture' && (await current()) === b.id && dockedRect.w > 0 && dockedRect.h > 0, JSON.stringify({ docked, dockedRect }), ); // 44n. The window's × closes the document, and the kind shows in the tabs again. await evaluate(`__gm.floatDocument(${JSON.stringify(`asset:${b.id}`)})`); const again = await windows(); await click(again[0].close.x, again[0].close.y); const closed = { windows: await windows(), docs: await docs() }; await showTab('texture'); check( 'the × closes the window and its document; the kind opens in a tab again', closed.windows.length === 0 && !closed.docs.docs.includes(`asset:${b.id}`) && (await json('__gm.tab()')) === 'texture' && (await json('__gm.texture.rect()')).w > 0, JSON.stringify({ again, closed, tab: await json('__gm.tab()') }), ); for (const doc of (await docs()).docs) await evaluate(`__gm.closeDocument(${JSON.stringify(doc)})`); await showTab('scene'); await mutate(` for (const { id, was } of ${JSON.stringify([a, b])}) __gm.state.project.assets.find((x) => x.id === id).name = was; __gm.state.preview(); `); }