Replace drag-promote gesture with Ctrl+Shift+P keyboard shortcut
This commit is contained in:
parent
8e4a358aa8
commit
5085326cb1
6 changed files with 142 additions and 373 deletions
|
|
@ -25,8 +25,7 @@ import {
|
|||
presetThreeColumns,
|
||||
presetTwoRows,
|
||||
presetTwoByTwo,
|
||||
promoteFromGutter,
|
||||
flattenLayout,
|
||||
promoteLeaf,
|
||||
type TreeNode,
|
||||
type LeafNode,
|
||||
type SplitNode,
|
||||
|
|
@ -504,129 +503,86 @@ describe("presets", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("promoteFromGutter", () => {
|
||||
it("HSplit(a, VSplit(b, c)) → VSplit(HSplit(a, b), c) when promoting the inner VSplit", () => {
|
||||
describe("promoteLeaf", () => {
|
||||
it("HSplit(a, VSplit(b, c)) + promote c → VSplit(HSplit(a, b), c)", () => {
|
||||
const a = newLeaf({ label: "a" });
|
||||
const b = newLeaf({ label: "b" });
|
||||
const c = newLeaf({ label: "c" });
|
||||
const inner = newSplit("v", b, c, 0.5);
|
||||
const tree = newSplit("h", a, inner, 0.5);
|
||||
|
||||
const next = promoteFromGutter(tree, inner.id) as SplitNode;
|
||||
expect(next.kind).toBe("split");
|
||||
expect(next.orientation).toBe("v"); // outer is now V (matches inner's old axis)
|
||||
// Top: HSplit(a, b)
|
||||
const tree = newSplit("h", a, newSplit("v", b, c, 0.5), 0.5);
|
||||
const next = promoteLeaf(tree, c.id) as SplitNode;
|
||||
expect(next.orientation).toBe("v");
|
||||
const top = next.a as SplitNode;
|
||||
expect(top.kind).toBe("split");
|
||||
expect(top.orientation).toBe("h");
|
||||
expect((top.a as LeafNode).label).toBe("a");
|
||||
expect((top.b as LeafNode).label).toBe("b");
|
||||
// Bottom: c
|
||||
expect((next.b as LeafNode).label).toBe("c");
|
||||
});
|
||||
|
||||
it("is its own inverse — applying it twice (to the moved inner) returns the original shape", () => {
|
||||
it("HSplit(a, VSplit(b, c)) + promote b → VSplit(b, HSplit(a, c))", () => {
|
||||
const a = newLeaf({ label: "a" });
|
||||
const b = newLeaf({ label: "b" });
|
||||
const c = newLeaf({ label: "c" });
|
||||
const inner = newSplit("v", b, c, 0.5);
|
||||
const tree = newSplit("h", a, inner, 0.5);
|
||||
|
||||
const promoted = promoteFromGutter(tree, inner.id) as SplitNode;
|
||||
// The new inner split (combined a+b) has a fresh id; find it via walk.
|
||||
const newInnerId = (promoted.a as SplitNode).id;
|
||||
const restored = promoteFromGutter(promoted, newInnerId) as SplitNode;
|
||||
const tree = newSplit("h", a, newSplit("v", b, c, 0.5), 0.5);
|
||||
const next = promoteLeaf(tree, b.id) as SplitNode;
|
||||
expect(next.orientation).toBe("v");
|
||||
expect((next.a as LeafNode).label).toBe("b");
|
||||
const bot = next.b as SplitNode;
|
||||
expect(bot.orientation).toBe("h");
|
||||
expect((bot.a as LeafNode).label).toBe("a");
|
||||
expect((bot.b as LeafNode).label).toBe("c");
|
||||
});
|
||||
|
||||
it("is self-inverse — promote c then promote a returns the original shape", () => {
|
||||
const a = newLeaf({ label: "a" });
|
||||
const b = newLeaf({ label: "b" });
|
||||
const c = newLeaf({ label: "c" });
|
||||
const tree = newSplit("h", a, newSplit("v", b, c, 0.5), 0.5);
|
||||
const promoted = promoteLeaf(tree, c.id)!;
|
||||
const restored = promoteLeaf(promoted, a.id) as SplitNode;
|
||||
expect(restored.orientation).toBe("h");
|
||||
expect((restored.a as LeafNode).label).toBe("a");
|
||||
const innerR = restored.b as SplitNode;
|
||||
expect(innerR.orientation).toBe("v");
|
||||
expect((innerR.a as LeafNode).label).toBe("b");
|
||||
expect((innerR.b as LeafNode).label).toBe("c");
|
||||
const inner = restored.b as SplitNode;
|
||||
expect(inner.orientation).toBe("v");
|
||||
expect((inner.a as LeafNode).label).toBe("b");
|
||||
expect((inner.b as LeafNode).label).toBe("c");
|
||||
});
|
||||
|
||||
it("mirror direction: VSplit(HSplit(a, b), c) → HSplit(a, VSplit(b, c))", () => {
|
||||
// S=HSplit (first child of outer V). isFirstInP=true.
|
||||
// Promoted = S.a = a. Sibling = c (P.b). Combined = VSplit(b, c).
|
||||
const a = newLeaf({ label: "a" });
|
||||
const b = newLeaf({ label: "b" });
|
||||
const c = newLeaf({ label: "c" });
|
||||
const inner = newSplit("h", a, b, 0.5);
|
||||
const tree = newSplit("v", inner, c, 0.5);
|
||||
|
||||
const next = promoteFromGutter(tree, inner.id) as SplitNode;
|
||||
expect(next.orientation).toBe("h");
|
||||
expect((next.a as LeafNode).label).toBe("a");
|
||||
const innerR = next.b as SplitNode;
|
||||
expect(innerR.orientation).toBe("v");
|
||||
expect((innerR.a as LeafNode).label).toBe("b");
|
||||
expect((innerR.b as LeafNode).label).toBe("c");
|
||||
it("returns null when the leaf has no parent (single-leaf root)", () => {
|
||||
const leaf = newLeaf();
|
||||
expect(promoteLeaf(leaf, leaf.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when the split has no parent (root)", () => {
|
||||
const root = newSplit("h", newLeaf(), newLeaf());
|
||||
expect(promoteFromGutter(root, root.id)).toBeNull();
|
||||
it("returns null when the leaf's parent is the root (no grandparent)", () => {
|
||||
const a = newLeaf();
|
||||
const b = newLeaf();
|
||||
const root = newSplit("h", a, b);
|
||||
expect(promoteLeaf(root, a.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when parent has the same orientation (gesture undefined)", () => {
|
||||
// Both axes H: there's no perpendicular promote.
|
||||
const inner = newSplit("h", newLeaf(), newLeaf());
|
||||
const root = newSplit("h", newLeaf(), inner);
|
||||
expect(promoteFromGutter(root, inner.id)).toBeNull();
|
||||
it("returns null when parent and grandparent share orientation", () => {
|
||||
const a = newLeaf();
|
||||
const b = newLeaf();
|
||||
const c = newLeaf();
|
||||
const inner = newSplit("h", b, c);
|
||||
const root = newSplit("h", a, inner);
|
||||
expect(promoteLeaf(root, b.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves all leaf ids (no PTYs respawn on promote)", () => {
|
||||
const a = newLeaf({ label: "a" });
|
||||
const b = newLeaf({ label: "b" });
|
||||
const c = newLeaf({ label: "c" });
|
||||
const inner = newSplit("v", b, c);
|
||||
const tree = newSplit("h", a, inner);
|
||||
const tree = newSplit("h", a, newSplit("v", b, c));
|
||||
const before = Array.from(walkLeaves(tree))
|
||||
.map((l) => l.id)
|
||||
.sort();
|
||||
const after = Array.from(walkLeaves(promoteFromGutter(tree, inner.id)!))
|
||||
const after = Array.from(walkLeaves(promoteLeaf(tree, c.id)!))
|
||||
.map((l) => l.id)
|
||||
.sort();
|
||||
expect(after).toEqual(before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("flattenLayout — promote metadata", () => {
|
||||
it("populates promote on the inner V-gutter of HSplit(a, VSplit(b, c))", () => {
|
||||
const a = newLeaf();
|
||||
const b = newLeaf();
|
||||
const c = newLeaf();
|
||||
const inner = newSplit("v", b, c, 0.5);
|
||||
const tree = newSplit("h", a, inner, 0.5);
|
||||
|
||||
const { gutters } = flattenLayout(tree);
|
||||
const innerGutter = gutters.find((g) => g.splitId === inner.id)!;
|
||||
expect(innerGutter.promote).toBeDefined();
|
||||
expect(innerGutter.promote!.exitDirection).toBe("left");
|
||||
// Sibling box = left half (a's area)
|
||||
expect(innerGutter.promote!.siblingBox.left).toBe(0);
|
||||
expect(innerGutter.promote!.siblingBox.width).toBeCloseTo(0.5);
|
||||
// Promoted box = bottom row, full width
|
||||
expect(innerGutter.promote!.promotedBox.top).toBeCloseTo(0.5);
|
||||
expect(innerGutter.promote!.promotedBox.height).toBeCloseTo(0.5);
|
||||
expect(innerGutter.promote!.promotedBox.left).toBe(0);
|
||||
expect(innerGutter.promote!.promotedBox.width).toBe(1);
|
||||
});
|
||||
|
||||
it("does NOT populate promote on the root gutter or on same-axis nestings", () => {
|
||||
const root = newSplit("h", newLeaf(), newLeaf());
|
||||
const { gutters: g1 } = flattenLayout(root);
|
||||
expect(g1[0].promote).toBeUndefined();
|
||||
|
||||
// Same-axis parent: no promote.
|
||||
const inner = newSplit("h", newLeaf(), newLeaf());
|
||||
const sameAxis = newSplit("h", newLeaf(), inner);
|
||||
const { gutters: g2 } = flattenLayout(sameAxis);
|
||||
const innerGutter = g2.find((g) => g.splitId === inner.id)!;
|
||||
expect(innerGutter.promote).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("serialize / deserialize", () => {
|
||||
it("roundtrips a complex tree", () => {
|
||||
const leaf1 = newLeaf({ distro: "Ubuntu", label: "left", broadcast: true });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue