From a5cb32c63cd9e4ea7f25a5cdf5196289379fa5e0 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Sat, 21 Mar 2026 14:12:47 -0400 Subject: [PATCH] Add `calculatePath` method --- src/layout/calculate_path.ts | 53 ++++++++++++++++++++++++ src/regular-layout.ts | 11 +++++ tests/unit/calculate_path.spec.ts | 69 +++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 src/layout/calculate_path.ts create mode 100644 tests/unit/calculate_path.spec.ts diff --git a/src/layout/calculate_path.ts b/src/layout/calculate_path.ts new file mode 100644 index 0000000..50e1c12 --- /dev/null +++ b/src/layout/calculate_path.ts @@ -0,0 +1,53 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import type { Layout } from "./types.ts"; + +/** + * Calculates the index path for a panel with the given name. + * + * Traverses the layout tree to find the named panel and returns the + * index path describing its position in the tree. + * + * @param name - The name of the panel to find. + * @param layout - The layout tree to search. + * @returns The panel's index path if found, `null` otherwise. + */ +export function calculate_path(name: string, layout: Layout): number[] | null { + return calculate_path_recursive(name, layout, []); +} + +function calculate_path_recursive( + name: string, + panel: Layout, + path: number[], +): number[] | null { + if (panel.type === "child-panel") { + if (!panel.tabs.includes(name)) { + return null; + } + + return path; + } + + for (let i = 0; i < panel.children.length; i++) { + const result = calculate_path_recursive(name, panel.children[i], [ + ...path, + i, + ]); + + if (result) { + return result; + } + } + + return null; +} diff --git a/src/regular-layout.ts b/src/regular-layout.ts index db80427..d958b4f 100644 --- a/src/regular-layout.ts +++ b/src/regular-layout.ts @@ -33,6 +33,7 @@ import { redistribute_panel_sizes } from "./layout/redistribute_panel_sizes.ts"; import { updateOverlaySheet } from "./layout/generate_overlay.ts"; import { calculate_edge } from "./layout/calculate_edge.ts"; import { flatten } from "./layout/flatten.ts"; +import { calculate_path } from "./layout/calculate_path.ts"; import { DEFAULT_PHYSICS, type PhysicsUpdate, @@ -151,6 +152,16 @@ export class RegularLayout extends HTMLElement { return calculate_intersection(col, row, this._panel); }; + /** + * Calculates the index path for a panel with the given name. + * + * @param name - The name of the panel to find. + * @returns The panel's index path if found, `null` otherwise. + */ + calculatePath = (name: string): number[] | null => { + return calculate_path(name, this._panel); + }; + /** * Sets the visual overlay state during drag-and-drop operations. * Displays a preview of where a panel would be placed at the given coordinates. diff --git a/tests/unit/calculate_path.spec.ts b/tests/unit/calculate_path.spec.ts new file mode 100644 index 0000000..42f4b1e --- /dev/null +++ b/tests/unit/calculate_path.spec.ts @@ -0,0 +1,69 @@ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░ +// ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░ +// ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░ +// ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃ +// ┃ * of the Regular Layout library, distributed under the terms of the * ┃ +// ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { expect, test } from "@playwright/test"; +import { calculate_path } from "../../src/layout/calculate_path.ts"; +import { LAYOUTS } from "../helpers/fixtures.ts"; + +test("returns null for a name not in the layout", () => { + const result = calculate_path("ZZZ", LAYOUTS.SINGLE_AAA); + expect(result).toBeNull(); +}); + +test("returns null for an empty layout", () => { + const result = calculate_path("AAA", { + type: "split-panel", + orientation: "horizontal", + sizes: [], + children: [], + }); + expect(result).toBeNull(); +}); + +test("finds a single panel", () => { + const result = calculate_path("AAA", LAYOUTS.SINGLE_AAA); + expect(result).toStrictEqual([]); +}); + +test("finds left panel in horizontal split", () => { + const result = calculate_path("AAA", LAYOUTS.TWO_HORIZONTAL); + expect(result).toStrictEqual([0]); +}); + +test("finds right panel in horizontal split", () => { + const result = calculate_path("BBB", LAYOUTS.TWO_HORIZONTAL); + expect(result).toStrictEqual([1]); +}); + +test("finds top panel in vertical split", () => { + const result = calculate_path("AAA", LAYOUTS.TWO_VERTICAL); + expect(result).toStrictEqual([0]); +}); + +test("finds panel in nested layout", () => { + const result = calculate_path("AAA", LAYOUTS.NESTED_BASIC); + expect(result).toStrictEqual([0, 0]); +}); + +test("finds deeply nested panel", () => { + const result = calculate_path("BBB", LAYOUTS.DEEPLY_NESTED); + expect(result).toStrictEqual([0, 1]); +}); + +test("finds non-selected tab by name", () => { + const result = calculate_path("BBB", LAYOUTS.SINGLE_TABS); + expect(result).toStrictEqual([]); +}); + +test("finds leaf panel in deeply nested alt layout", () => { + const result = calculate_path("DDD", LAYOUTS.DEEPLY_NESTED_ALT); + expect(result).toStrictEqual([1]); +});