gloomberb 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -19
- package/bin/gloomberb +3 -0
- package/package.json +5 -4
- package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
- package/src/app.tsx +1330 -0
- package/src/brokers/sync-broker-instance.test.ts +100 -0
- package/src/brokers/sync-broker-instance.ts +341 -0
- package/src/cli/commands/plugins.ts +173 -0
- package/src/cli/commands/portfolio.ts +210 -0
- package/src/cli/commands/search.ts +120 -0
- package/src/cli/commands/ticker.ts +375 -0
- package/src/cli/commands/watchlist.ts +210 -0
- package/src/cli/context.ts +62 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/helpers.ts +85 -0
- package/src/cli/index.ts +143 -0
- package/src/cli/ticker-resolution.ts +39 -0
- package/src/cli/types.ts +15 -0
- package/src/cli.test.ts +361 -0
- package/src/components/chart/chart-data.ts +239 -0
- package/src/components/chart/chart-renderer.ts +1199 -0
- package/src/components/chart/chart-types.ts +82 -0
- package/src/components/chart/chart-viewport.ts +21 -0
- package/src/components/chart/chart.test.ts +439 -0
- package/src/components/chart/comparison-chart-data.test.ts +109 -0
- package/src/components/chart/comparison-chart-data.ts +316 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
- package/src/components/chart/comparison-chart-renderer.ts +354 -0
- package/src/components/chart/comparison-stock-chart.tsx +1486 -0
- package/src/components/chart/cursor-motion.ts +48 -0
- package/src/components/chart/native/chart-rasterizer.ts +732 -0
- package/src/components/chart/native/kitty-adapter.ts +12 -0
- package/src/components/chart/native/kitty-manager.ts +142 -0
- package/src/components/chart/native/kitty-protocol.ts +110 -0
- package/src/components/chart/native/kitty-support.ts +71 -0
- package/src/components/chart/native/kitty.test.ts +620 -0
- package/src/components/chart/native/renderer-selection.ts +24 -0
- package/src/components/chart/native/surface-manager.ts +225 -0
- package/src/components/chart/native/surface-sync.ts +34 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
- package/src/components/chart/stock-chart.test.ts +107 -0
- package/src/components/chart/stock-chart.tsx +1556 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
- package/src/components/command-bar/command-bar.test.tsx +1524 -0
- package/src/components/command-bar/command-bar.tsx +4823 -0
- package/src/components/command-bar/command-registry.ts +261 -0
- package/src/components/command-bar/helpers.test.ts +224 -0
- package/src/components/command-bar/helpers.ts +371 -0
- package/src/components/command-bar/root-shortcuts.ts +158 -0
- package/src/components/command-bar/view-model.test.ts +152 -0
- package/src/components/command-bar/view-model.ts +185 -0
- package/src/components/command-bar/workflow-ops.test.ts +70 -0
- package/src/components/command-bar/workflow-ops.ts +433 -0
- package/src/components/command-bar/workflow-types.ts +127 -0
- package/src/components/detail-feed-view.test.tsx +105 -0
- package/src/components/detail-feed-view.tsx +295 -0
- package/src/components/index.ts +32 -0
- package/src/components/layout/floating-pane.tsx +49 -0
- package/src/components/layout/header.tsx +129 -0
- package/src/components/layout/pane-header.tsx +44 -0
- package/src/components/layout/pane.tsx +49 -0
- package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
- package/src/components/layout/pixel-mouse.test.ts +40 -0
- package/src/components/layout/shell.test.tsx +625 -0
- package/src/components/layout/shell.tsx +1200 -0
- package/src/components/layout/status-bar.test.tsx +173 -0
- package/src/components/layout/status-bar.tsx +138 -0
- package/src/components/markdown-editor.tsx +134 -0
- package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
- package/src/components/pane-settings-dialog.tsx +468 -0
- package/src/components/pane-template-wizard.test.tsx +52 -0
- package/src/components/pane-template-wizard.tsx +199 -0
- package/src/components/price-selector-dialog.tsx +195 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/tab-bar.tsx +17 -0
- package/src/components/ticker-badge-text.test.tsx +160 -0
- package/src/components/ticker-badge-text.tsx +58 -0
- package/src/components/ticker-badge.tsx +88 -0
- package/src/components/ticker-list-table.tsx +188 -0
- package/src/components/toggle-list.tsx +74 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/data-table.tsx +210 -0
- package/src/components/ui/external-link.tsx +22 -0
- package/src/components/ui/fields.tsx +187 -0
- package/src/components/ui/frame.tsx +73 -0
- package/src/components/ui/index.ts +53 -0
- package/src/components/ui/list-view.tsx +166 -0
- package/src/components/ui/loading.tsx +70 -0
- package/src/components/ui/status.tsx +83 -0
- package/src/components/ui/tabs.tsx +47 -0
- package/src/components/ui/toggle.tsx +164 -0
- package/src/components/ui/ui.test.tsx +134 -0
- package/src/data/app-persistence.ts +25 -0
- package/src/data/config-store.test.ts +310 -0
- package/src/data/config-store.ts +469 -0
- package/src/data/plugin-state-store.ts +102 -0
- package/src/data/resource-store.ts +323 -0
- package/src/data/session-store.ts +52 -0
- package/src/data/sqlite-cache.test.ts +103 -0
- package/src/data/sqlite-database.ts +62 -0
- package/src/data/sqlite-json.ts +19 -0
- package/src/data/ticker-repository.ts +45 -0
- package/src/data/ticker-store.ts +46 -0
- package/src/index.tsx +67 -0
- package/src/market-data/coordinator.test.ts +369 -0
- package/src/market-data/coordinator.ts +580 -0
- package/src/market-data/hooks.tsx +234 -0
- package/src/market-data/query-store.ts +28 -0
- package/src/market-data/request-types.test.ts +64 -0
- package/src/market-data/request-types.ts +70 -0
- package/src/market-data/result-types.ts +56 -0
- package/src/market-data/selectors.ts +166 -0
- package/src/market-data/trace.ts +7 -0
- package/src/pane-settings.test.ts +71 -0
- package/src/pane-settings.ts +73 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
- package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
- package/src/plugins/builtin/ai/index.test.tsx +139 -0
- package/src/plugins/builtin/ai/index.tsx +86 -0
- package/src/plugins/builtin/ai/providers.ts +99 -0
- package/src/plugins/builtin/ai/runner.ts +82 -0
- package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
- package/src/plugins/builtin/ai/screener-contract.ts +174 -0
- package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
- package/src/plugins/builtin/ai/settings.ts +79 -0
- package/src/plugins/builtin/ai/ticker-context.ts +64 -0
- package/src/plugins/builtin/ai/utils.ts +71 -0
- package/src/plugins/builtin/ask-ai.test.tsx +258 -0
- package/src/plugins/builtin/ask-ai.tsx +9 -0
- package/src/plugins/builtin/chat-controller.test.ts +217 -0
- package/src/plugins/builtin/chat-controller.ts +340 -0
- package/src/plugins/builtin/chat.test.tsx +451 -0
- package/src/plugins/builtin/chat.tsx +620 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
- package/src/plugins/builtin/comparison-chart.tsx +169 -0
- package/src/plugins/builtin/data-providers.ts +16 -0
- package/src/plugins/builtin/debug.tsx +351 -0
- package/src/plugins/builtin/help.test.tsx +110 -0
- package/src/plugins/builtin/help.tsx +281 -0
- package/src/plugins/builtin/layout-manager.tsx +264 -0
- package/src/plugins/builtin/manual-entry.tsx +25 -0
- package/src/plugins/builtin/news.tsx +143 -0
- package/src/plugins/builtin/notes-files.ts +58 -0
- package/src/plugins/builtin/notes.tsx +396 -0
- package/src/plugins/builtin/options-availability.test.tsx +179 -0
- package/src/plugins/builtin/options-availability.ts +158 -0
- package/src/plugins/builtin/options.tsx +364 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
- package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
- package/src/plugins/builtin/portfolio-list.test.ts +74 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
- package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
- package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
- package/src/plugins/builtin/sec.tsx +250 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
- package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
- package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
- package/src/plugins/builtin/yahoo.ts +18 -0
- package/src/plugins/event-bus.ts +37 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
- package/src/plugins/ibkr/account-cache.test.ts +101 -0
- package/src/plugins/ibkr/account-cache.ts +137 -0
- package/src/plugins/ibkr/broker-adapter.ts +168 -0
- package/src/plugins/ibkr/config.test.ts +68 -0
- package/src/plugins/ibkr/config.ts +262 -0
- package/src/plugins/ibkr/dialogs.tsx +83 -0
- package/src/plugins/ibkr/flex.test.ts +107 -0
- package/src/plugins/ibkr/flex.ts +267 -0
- package/src/plugins/ibkr/fundamental-parser.ts +149 -0
- package/src/plugins/ibkr/gateway-helpers.ts +28 -0
- package/src/plugins/ibkr/gateway-selection.ts +51 -0
- package/src/plugins/ibkr/gateway-service.test.ts +450 -0
- package/src/plugins/ibkr/gateway-service.ts +2228 -0
- package/src/plugins/ibkr/index.test.tsx +183 -0
- package/src/plugins/ibkr/index.tsx +153 -0
- package/src/plugins/ibkr/instance-selection.test.ts +121 -0
- package/src/plugins/ibkr/instance-selection.ts +36 -0
- package/src/plugins/ibkr/price-normalization.test.ts +38 -0
- package/src/plugins/ibkr/price-normalization.ts +59 -0
- package/src/plugins/ibkr/trade-badge.tsx +27 -0
- package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
- package/src/plugins/ibkr/trade-tab.tsx +1050 -0
- package/src/plugins/ibkr/trade-utils.test.ts +175 -0
- package/src/plugins/ibkr/trade-utils.ts +149 -0
- package/src/plugins/ibkr/trading-pane.tsx +438 -0
- package/src/plugins/ibkr/trading-state.test.ts +109 -0
- package/src/plugins/ibkr/trading-state.ts +339 -0
- package/src/plugins/loader.ts +71 -0
- package/src/plugins/pane-manager.test.ts +103 -0
- package/src/plugins/pane-manager.ts +974 -0
- package/src/plugins/plugin-persistence.ts +85 -0
- package/src/plugins/plugin-runtime.test.tsx +185 -0
- package/src/plugins/plugin-runtime.tsx +175 -0
- package/src/plugins/prediction-markets/cache.ts +114 -0
- package/src/plugins/prediction-markets/categories.ts +223 -0
- package/src/plugins/prediction-markets/chart.tsx +512 -0
- package/src/plugins/prediction-markets/columns.ts +133 -0
- package/src/plugins/prediction-markets/controller-data.ts +614 -0
- package/src/plugins/prediction-markets/controller.ts +725 -0
- package/src/plugins/prediction-markets/detail/book.tsx +118 -0
- package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
- package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
- package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
- package/src/plugins/prediction-markets/detail.test.tsx +47 -0
- package/src/plugins/prediction-markets/index.tsx +102 -0
- package/src/plugins/prediction-markets/keyboard.ts +64 -0
- package/src/plugins/prediction-markets/launch.test.ts +126 -0
- package/src/plugins/prediction-markets/launch.ts +240 -0
- package/src/plugins/prediction-markets/metrics.ts +311 -0
- package/src/plugins/prediction-markets/navigation.ts +90 -0
- package/src/plugins/prediction-markets/outcome-order.ts +19 -0
- package/src/plugins/prediction-markets/pane.test.tsx +425 -0
- package/src/plugins/prediction-markets/pane.tsx +294 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
- package/src/plugins/prediction-markets/rows.ts +229 -0
- package/src/plugins/prediction-markets/services/fetch.ts +97 -0
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
- package/src/plugins/prediction-markets/settings.ts +164 -0
- package/src/plugins/prediction-markets/status.ts +24 -0
- package/src/plugins/prediction-markets/table.tsx +84 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
- package/src/plugins/prediction-markets/types.ts +186 -0
- package/src/plugins/registry.ts +633 -0
- package/src/sources/exchange-rates.ts +18 -0
- package/src/sources/gloomberb-cloud.test.ts +199 -0
- package/src/sources/gloomberb-cloud.ts +326 -0
- package/src/sources/provider-errors.ts +19 -0
- package/src/sources/provider-router.test.ts +1476 -0
- package/src/sources/provider-router.ts +1405 -0
- package/src/sources/sec-edgar.test.ts +286 -0
- package/src/sources/sec-edgar.ts +526 -0
- package/src/sources/yahoo-finance.test.ts +14 -0
- package/src/sources/yahoo-finance.ts +1017 -0
- package/src/state/app-activity.test.ts +41 -0
- package/src/state/app-activity.ts +106 -0
- package/src/state/app-bootstrap.test.ts +471 -0
- package/src/state/app-bootstrap.ts +308 -0
- package/src/state/app-context.test.ts +406 -0
- package/src/state/app-context.test.tsx +132 -0
- package/src/state/app-context.tsx +969 -0
- package/src/state/selectors.ts +67 -0
- package/src/state/session-persistence.test.ts +128 -0
- package/src/state/session-persistence.ts +161 -0
- package/src/state/ticker-refresh-queue.test.ts +46 -0
- package/src/state/ticker-refresh-queue.ts +40 -0
- package/src/state/use-inline-tickers.ts +174 -0
- package/src/state/use-quote-streaming.test.ts +37 -0
- package/src/state/use-quote-streaming.test.tsx +70 -0
- package/src/state/use-quote-streaming.ts +80 -0
- package/src/state/use-session-snapshot.ts +84 -0
- package/src/test-support/data-provider.ts +22 -0
- package/src/theme/color-utils.ts +67 -0
- package/src/theme/colors.ts +128 -0
- package/src/theme/themes.test.ts +45 -0
- package/src/theme/themes.ts +491 -0
- package/src/types/broker.ts +105 -0
- package/src/types/config.ts +434 -0
- package/src/types/data-provider.ts +81 -0
- package/src/types/financials.ts +142 -0
- package/src/types/instrument.ts +29 -0
- package/src/types/persistence.ts +21 -0
- package/src/types/plugin.ts +354 -0
- package/src/types/ticker.ts +58 -0
- package/src/types/trading.ts +85 -0
- package/src/updater.test.ts +162 -0
- package/src/updater.ts +247 -0
- package/src/utils/api-client.ts +666 -0
- package/src/utils/ascii-chart.ts +172 -0
- package/src/utils/broker-instances.ts +47 -0
- package/src/utils/cli-output.ts +83 -0
- package/src/utils/currency-units.test.ts +49 -0
- package/src/utils/currency-units.ts +82 -0
- package/src/utils/debug-log.ts +133 -0
- package/src/utils/exchange-rate-map.ts +8 -0
- package/src/utils/format.ts +158 -0
- package/src/utils/fuzzy-search.ts +34 -0
- package/src/utils/market-status.ts +95 -0
- package/src/utils/options.ts +86 -0
- package/src/utils/price-history.ts +39 -0
- package/src/utils/quote-time.test.ts +22 -0
- package/src/utils/quote-time.ts +50 -0
- package/src/utils/sec.test.ts +55 -0
- package/src/utils/sec.ts +45 -0
- package/src/utils/ticker-list.test.ts +22 -0
- package/src/utils/ticker-list.ts +35 -0
- package/src/utils/ticker-metadata.ts +51 -0
- package/src/utils/ticker-search.test.ts +227 -0
- package/src/utils/ticker-search.ts +764 -0
- package/src/utils/ticker-tokenizer.test.ts +48 -0
- package/src/utils/ticker-tokenizer.ts +72 -0
- package/src/version.ts +1 -0
- package/install.mjs +0 -68
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DockLayoutNode,
|
|
3
|
+
DockedPlacementMemory,
|
|
4
|
+
FloatingPaneEntry,
|
|
5
|
+
FloatingPlacementMemory,
|
|
6
|
+
LayoutConfig,
|
|
7
|
+
PaneInstanceConfig,
|
|
8
|
+
} from "../types/config";
|
|
9
|
+
import type { PaneDef } from "../types/plugin";
|
|
10
|
+
import {
|
|
11
|
+
clonePlacementMemory,
|
|
12
|
+
createPaneInstance,
|
|
13
|
+
findPaneInstance,
|
|
14
|
+
normalizePaneLayout,
|
|
15
|
+
removePaneInstances,
|
|
16
|
+
type DockSplitNode,
|
|
17
|
+
} from "../types/config";
|
|
18
|
+
|
|
19
|
+
export const MIN_PANE_WIDTH = 20;
|
|
20
|
+
export const MIN_FLOAT_WIDTH = 15;
|
|
21
|
+
export const MIN_FLOAT_HEIGHT = 6;
|
|
22
|
+
export const MIN_DOCKED_HEIGHT = 5;
|
|
23
|
+
|
|
24
|
+
export interface ResolvedPane {
|
|
25
|
+
instance: PaneInstanceConfig;
|
|
26
|
+
def: PaneDef;
|
|
27
|
+
floating?: FloatingPaneEntry;
|
|
28
|
+
path?: Array<0 | 1>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DockTarget {
|
|
32
|
+
relativeTo: string;
|
|
33
|
+
position: "left" | "right" | "above" | "below";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type GlobalDockRegion = "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
37
|
+
export type LeafDropPosition =
|
|
38
|
+
| "left"
|
|
39
|
+
| "right"
|
|
40
|
+
| "top"
|
|
41
|
+
| "bottom"
|
|
42
|
+
| "center"
|
|
43
|
+
| "top-left"
|
|
44
|
+
| "top-right"
|
|
45
|
+
| "bottom-left"
|
|
46
|
+
| "bottom-right";
|
|
47
|
+
|
|
48
|
+
export type DropTarget =
|
|
49
|
+
| { kind: "frame"; edge: "left" | "right" | "top" | "bottom" }
|
|
50
|
+
| { kind: "leaf"; targetId: string; position: LeafDropPosition };
|
|
51
|
+
|
|
52
|
+
export interface FloatingRect {
|
|
53
|
+
x: number;
|
|
54
|
+
y: number;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
zIndex?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LayoutBounds {
|
|
61
|
+
x: number;
|
|
62
|
+
y: number;
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DockLeafRef {
|
|
68
|
+
instanceId: string;
|
|
69
|
+
path: Array<0 | 1>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DockLeafLayout extends DockLeafRef {
|
|
73
|
+
rect: LayoutBounds;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DockDividerLayout {
|
|
77
|
+
path: Array<0 | 1>;
|
|
78
|
+
axis: DockSplitNode["axis"];
|
|
79
|
+
rect: LayoutBounds;
|
|
80
|
+
bounds: LayoutBounds;
|
|
81
|
+
ratio: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LayoutSimulation {
|
|
85
|
+
layout: LayoutConfig;
|
|
86
|
+
previewRect: LayoutBounds | null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clampRatio(ratio: number): number {
|
|
90
|
+
if (!Number.isFinite(ratio)) return 0.5;
|
|
91
|
+
return Math.max(0.1, Math.min(0.9, ratio));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cloneDockNode(node: DockLayoutNode): DockLayoutNode {
|
|
95
|
+
if (node.kind === "pane") {
|
|
96
|
+
return { kind: "pane", instanceId: node.instanceId };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
kind: "split",
|
|
100
|
+
axis: node.axis,
|
|
101
|
+
ratio: node.ratio,
|
|
102
|
+
first: cloneDockNode(node.first),
|
|
103
|
+
second: cloneDockNode(node.second),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ensurePaneInstance(layout: LayoutConfig, instance: PaneInstanceConfig): LayoutConfig {
|
|
108
|
+
if (layout.instances.some((entry) => entry.instanceId === instance.instanceId)) return layout;
|
|
109
|
+
return { ...layout, instances: [...layout.instances, instance] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clampFloatingRect(rect: FloatingRect, termWidth?: number, termHeight?: number): FloatingRect {
|
|
113
|
+
const width = Math.max(MIN_FLOAT_WIDTH, Math.round(rect.width));
|
|
114
|
+
const height = Math.max(MIN_FLOAT_HEIGHT, Math.round(rect.height));
|
|
115
|
+
const maxX = typeof termWidth === "number" ? Math.max(0, termWidth - width) : Number.POSITIVE_INFINITY;
|
|
116
|
+
const maxY = typeof termHeight === "number" ? Math.max(0, termHeight - height) : Number.POSITIVE_INFINITY;
|
|
117
|
+
return {
|
|
118
|
+
x: Math.max(0, Math.min(Math.round(rect.x), maxX)),
|
|
119
|
+
y: Math.max(0, Math.min(Math.round(rect.y), maxY)),
|
|
120
|
+
width,
|
|
121
|
+
height,
|
|
122
|
+
zIndex: rect.zIndex,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function defaultFloatingRect(termWidth: number, termHeight: number, def?: PaneDef): FloatingRect {
|
|
127
|
+
const width = def?.defaultFloatingSize?.width ?? Math.floor(termWidth * 0.6);
|
|
128
|
+
const height = def?.defaultFloatingSize?.height ?? Math.floor(termHeight * 0.6);
|
|
129
|
+
return clampFloatingRect({
|
|
130
|
+
x: Math.floor((termWidth - width) / 2),
|
|
131
|
+
y: Math.floor((termHeight - height) / 2),
|
|
132
|
+
width,
|
|
133
|
+
height,
|
|
134
|
+
}, termWidth, termHeight);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function maxFloatingZ(layout: LayoutConfig): number {
|
|
138
|
+
return layout.floating.reduce((highest, entry) => Math.max(highest, entry.zIndex ?? 50), 50);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function collectDockLeafRefs(node: DockLayoutNode | null, path: Array<0 | 1> = [], result: DockLeafRef[] = []): DockLeafRef[] {
|
|
142
|
+
if (!node) return result;
|
|
143
|
+
if (node.kind === "pane") {
|
|
144
|
+
result.push({ instanceId: node.instanceId, path });
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
collectDockLeafRefs(node.first, [...path, 0], result);
|
|
148
|
+
collectDockLeafRefs(node.second, [...path, 1], result);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function getDockedInstanceIds(node: DockLayoutNode | null): string[] {
|
|
153
|
+
return collectDockLeafRefs(node).map((entry) => entry.instanceId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getRepresentativeLeafId(node: DockLayoutNode, preferStart: boolean): string {
|
|
157
|
+
if (node.kind === "pane") return node.instanceId;
|
|
158
|
+
return getRepresentativeLeafId(preferStart ? node.first : node.second, preferStart);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getNodeAtPath(node: DockLayoutNode | null, path: Array<0 | 1>): DockLayoutNode | null {
|
|
162
|
+
let current = node;
|
|
163
|
+
for (const segment of path) {
|
|
164
|
+
if (!current || current.kind !== "split") return null;
|
|
165
|
+
current = segment === 0 ? current.first : current.second;
|
|
166
|
+
}
|
|
167
|
+
return current;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function replaceNodeAtPath(node: DockLayoutNode, path: Array<0 | 1>, replacement: DockLayoutNode): DockLayoutNode {
|
|
171
|
+
if (path.length === 0) return replacement;
|
|
172
|
+
if (node.kind !== "split") return node;
|
|
173
|
+
const [head, ...rest] = path;
|
|
174
|
+
if (head === 0) {
|
|
175
|
+
return {
|
|
176
|
+
...node,
|
|
177
|
+
first: replaceNodeAtPath(node.first, rest as Array<0 | 1>, replacement),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
...node,
|
|
182
|
+
second: replaceNodeAtPath(node.second, rest as Array<0 | 1>, replacement),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function removeNodeAtPath(node: DockLayoutNode | null, path: Array<0 | 1>): DockLayoutNode | null {
|
|
187
|
+
if (!node) return null;
|
|
188
|
+
if (path.length === 0) return null;
|
|
189
|
+
if (node.kind !== "split") return node;
|
|
190
|
+
|
|
191
|
+
const [head, ...rest] = path;
|
|
192
|
+
if (head === 0) {
|
|
193
|
+
const first = removeNodeAtPath(node.first, rest as Array<0 | 1>);
|
|
194
|
+
if (!first) return node.second;
|
|
195
|
+
return { ...node, first };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const second = removeNodeAtPath(node.second, rest as Array<0 | 1>);
|
|
199
|
+
if (!second) return node.first;
|
|
200
|
+
return { ...node, second };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeFloatingEntries(layout: LayoutConfig): FloatingPaneEntry[] {
|
|
204
|
+
const dockedIds = new Set(getDockedInstanceIds(layout.dockRoot));
|
|
205
|
+
const seen = new Set<string>();
|
|
206
|
+
return layout.floating
|
|
207
|
+
.filter((entry) => !dockedIds.has(entry.instanceId))
|
|
208
|
+
.filter((entry) => {
|
|
209
|
+
if (seen.has(entry.instanceId)) return false;
|
|
210
|
+
seen.add(entry.instanceId);
|
|
211
|
+
return true;
|
|
212
|
+
})
|
|
213
|
+
.map((entry) => ({ ...entry }));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function captureDockedMemory(layout: LayoutConfig, dockRoot: DockLayoutNode | null): Map<string, DockedPlacementMemory> {
|
|
217
|
+
const memory = new Map<string, DockedPlacementMemory>();
|
|
218
|
+
const leaves = collectDockLeafRefs(dockRoot);
|
|
219
|
+
|
|
220
|
+
for (const leaf of leaves) {
|
|
221
|
+
if (leaf.path.length === 0) {
|
|
222
|
+
memory.set(leaf.instanceId, { path: [] });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const parentPath = leaf.path.slice(0, -1);
|
|
227
|
+
const branch = leaf.path[leaf.path.length - 1]!;
|
|
228
|
+
const parent = getNodeAtPath(dockRoot, parentPath);
|
|
229
|
+
if (!parent || parent.kind !== "split") {
|
|
230
|
+
memory.set(leaf.instanceId, { path: [...leaf.path] });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const sibling = branch === 0 ? parent.second : parent.first;
|
|
235
|
+
const position = parent.axis === "horizontal"
|
|
236
|
+
? (branch === 0 ? "left" : "right")
|
|
237
|
+
: (branch === 0 ? "above" : "below");
|
|
238
|
+
|
|
239
|
+
memory.set(leaf.instanceId, {
|
|
240
|
+
path: [...leaf.path],
|
|
241
|
+
anchorInstanceId: getRepresentativeLeafId(sibling, branch === 0),
|
|
242
|
+
position,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return memory;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function capturePlacementMemory(layout: LayoutConfig): LayoutConfig {
|
|
250
|
+
const dockedMemory = captureDockedMemory(layout, layout.dockRoot);
|
|
251
|
+
const floatingById = new Map(layout.floating.map((entry) => [entry.instanceId, entry] as const));
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...layout,
|
|
255
|
+
instances: layout.instances.map((instance) => {
|
|
256
|
+
const previous = clonePlacementMemory(instance.placementMemory) ?? {};
|
|
257
|
+
const nextDocked = dockedMemory.get(instance.instanceId) ?? previous.docked;
|
|
258
|
+
const floating = floatingById.get(instance.instanceId);
|
|
259
|
+
const nextFloating = floating ? {
|
|
260
|
+
x: floating.x,
|
|
261
|
+
y: floating.y,
|
|
262
|
+
width: floating.width,
|
|
263
|
+
height: floating.height,
|
|
264
|
+
} : previous.floating;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
...instance,
|
|
268
|
+
placementMemory: nextDocked || nextFloating ? {
|
|
269
|
+
docked: nextDocked,
|
|
270
|
+
floating: nextFloating,
|
|
271
|
+
} : undefined,
|
|
272
|
+
};
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function finalizeLayout(layout: LayoutConfig): LayoutConfig {
|
|
278
|
+
const normalized = normalizePaneLayout({
|
|
279
|
+
...layout,
|
|
280
|
+
floating: normalizeFloatingEntries(layout),
|
|
281
|
+
});
|
|
282
|
+
return capturePlacementMemory(normalized);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function buildSplitAroundNode(
|
|
286
|
+
existing: DockLayoutNode,
|
|
287
|
+
instanceId: string,
|
|
288
|
+
position: DockTarget["position"],
|
|
289
|
+
): DockLayoutNode {
|
|
290
|
+
const dragged: DockLayoutNode = { kind: "pane", instanceId };
|
|
291
|
+
const axis = position === "left" || position === "right" ? "horizontal" : "vertical";
|
|
292
|
+
const first = position === "left" || position === "above" ? dragged : existing;
|
|
293
|
+
const second = position === "left" || position === "above" ? existing : dragged;
|
|
294
|
+
return {
|
|
295
|
+
kind: "split",
|
|
296
|
+
axis,
|
|
297
|
+
ratio: 0.5,
|
|
298
|
+
first,
|
|
299
|
+
second,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function removeDockedLeaf(layout: LayoutConfig, instanceId: string): LayoutConfig {
|
|
304
|
+
const leaf = findDockLeaf(layout, instanceId);
|
|
305
|
+
if (!leaf) return layout;
|
|
306
|
+
return {
|
|
307
|
+
...layout,
|
|
308
|
+
dockRoot: removeNodeAtPath(layout.dockRoot, leaf.path),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function detachPane(layout: LayoutConfig, instanceId: string): LayoutConfig {
|
|
313
|
+
return {
|
|
314
|
+
...removeDockedLeaf(layout, instanceId),
|
|
315
|
+
floating: layout.floating.filter((entry) => entry.instanceId !== instanceId),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function insertRelativeToSubtreePath(
|
|
320
|
+
layout: LayoutConfig,
|
|
321
|
+
instanceId: string,
|
|
322
|
+
targetPath: Array<0 | 1>,
|
|
323
|
+
position: DockTarget["position"],
|
|
324
|
+
): LayoutConfig {
|
|
325
|
+
const base = detachPane(layout, instanceId);
|
|
326
|
+
if (!base.dockRoot) {
|
|
327
|
+
return finalizeLayout({ ...base, dockRoot: { kind: "pane", instanceId } });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let candidatePath = [...targetPath];
|
|
331
|
+
let targetNode = getNodeAtPath(base.dockRoot, candidatePath);
|
|
332
|
+
while (!targetNode && candidatePath.length > 0) {
|
|
333
|
+
candidatePath = candidatePath.slice(0, -1);
|
|
334
|
+
targetNode = getNodeAtPath(base.dockRoot, candidatePath);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!targetNode) {
|
|
338
|
+
return finalizeLayout({
|
|
339
|
+
...base,
|
|
340
|
+
dockRoot: buildSplitAroundNode(base.dockRoot, instanceId, position),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const replacement = buildSplitAroundNode(targetNode, instanceId, position);
|
|
345
|
+
return finalizeLayout({
|
|
346
|
+
...base,
|
|
347
|
+
dockRoot: replaceNodeAtPath(base.dockRoot, candidatePath, replacement),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function scoreDirectionalCandidate(
|
|
352
|
+
current: DockLeafLayout,
|
|
353
|
+
candidate: DockLeafLayout,
|
|
354
|
+
direction: DockTarget["position"],
|
|
355
|
+
): number | null {
|
|
356
|
+
const currentCenterX = current.rect.x + Math.floor(current.rect.width / 2);
|
|
357
|
+
const currentCenterY = current.rect.y + Math.floor(current.rect.height / 2);
|
|
358
|
+
const candidateCenterX = candidate.rect.x + Math.floor(candidate.rect.width / 2);
|
|
359
|
+
const candidateCenterY = candidate.rect.y + Math.floor(candidate.rect.height / 2);
|
|
360
|
+
|
|
361
|
+
if (direction === "left" && candidateCenterX >= currentCenterX) return null;
|
|
362
|
+
if (direction === "right" && candidateCenterX <= currentCenterX) return null;
|
|
363
|
+
if (direction === "above" && candidateCenterY >= currentCenterY) return null;
|
|
364
|
+
if (direction === "below" && candidateCenterY <= currentCenterY) return null;
|
|
365
|
+
|
|
366
|
+
const primaryDelta = direction === "left" || direction === "right"
|
|
367
|
+
? Math.abs(currentCenterX - candidateCenterX)
|
|
368
|
+
: Math.abs(currentCenterY - candidateCenterY);
|
|
369
|
+
const secondaryDelta = direction === "left" || direction === "right"
|
|
370
|
+
? Math.abs(currentCenterY - candidateCenterY)
|
|
371
|
+
: Math.abs(currentCenterX - candidateCenterX);
|
|
372
|
+
|
|
373
|
+
return primaryDelta * 1000 + secondaryDelta;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveSplitSizes(total: number, ratio: number, minSize: number): [number, number] {
|
|
377
|
+
if (total <= 1) return [1, 0];
|
|
378
|
+
if (total < minSize * 2) {
|
|
379
|
+
const first = Math.max(1, Math.floor(total / 2));
|
|
380
|
+
return [first, Math.max(1, total - first)];
|
|
381
|
+
}
|
|
382
|
+
const preferred = Math.round(total * clampRatio(ratio));
|
|
383
|
+
const first = Math.max(minSize, Math.min(total - minSize, preferred));
|
|
384
|
+
return [first, total - first];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function collectDockGeometry(
|
|
388
|
+
node: DockLayoutNode | null,
|
|
389
|
+
bounds: LayoutBounds,
|
|
390
|
+
path: Array<0 | 1> = [],
|
|
391
|
+
leaves: DockLeafLayout[] = [],
|
|
392
|
+
dividers: DockDividerLayout[] = [],
|
|
393
|
+
): { leaves: DockLeafLayout[]; dividers: DockDividerLayout[] } {
|
|
394
|
+
if (!node) return { leaves, dividers };
|
|
395
|
+
if (node.kind === "pane") {
|
|
396
|
+
leaves.push({ instanceId: node.instanceId, path, rect: { ...bounds } });
|
|
397
|
+
return { leaves, dividers };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (node.axis === "horizontal") {
|
|
401
|
+
const [firstWidth, secondWidth] = resolveSplitSizes(bounds.width, node.ratio, MIN_PANE_WIDTH);
|
|
402
|
+
const firstBounds = { x: bounds.x, y: bounds.y, width: firstWidth, height: bounds.height };
|
|
403
|
+
const secondBounds = { x: bounds.x + firstWidth, y: bounds.y, width: secondWidth, height: bounds.height };
|
|
404
|
+
dividers.push({
|
|
405
|
+
path,
|
|
406
|
+
axis: node.axis,
|
|
407
|
+
bounds: { ...bounds },
|
|
408
|
+
ratio: node.ratio,
|
|
409
|
+
rect: {
|
|
410
|
+
x: bounds.x + firstWidth - 1,
|
|
411
|
+
y: bounds.y,
|
|
412
|
+
width: 1,
|
|
413
|
+
height: bounds.height,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers);
|
|
417
|
+
collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers);
|
|
418
|
+
return { leaves, dividers };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const [firstHeight, secondHeight] = resolveSplitSizes(bounds.height, node.ratio, MIN_DOCKED_HEIGHT);
|
|
422
|
+
const firstBounds = { x: bounds.x, y: bounds.y, width: bounds.width, height: firstHeight };
|
|
423
|
+
const secondBounds = { x: bounds.x, y: bounds.y + firstHeight, width: bounds.width, height: secondHeight };
|
|
424
|
+
dividers.push({
|
|
425
|
+
path,
|
|
426
|
+
axis: node.axis,
|
|
427
|
+
bounds: { ...bounds },
|
|
428
|
+
ratio: node.ratio,
|
|
429
|
+
rect: {
|
|
430
|
+
x: bounds.x,
|
|
431
|
+
y: bounds.y + firstHeight - 1,
|
|
432
|
+
width: bounds.width,
|
|
433
|
+
height: 1,
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
collectDockGeometry(node.first, firstBounds, [...path, 0], leaves, dividers);
|
|
437
|
+
collectDockGeometry(node.second, secondBounds, [...path, 1], leaves, dividers);
|
|
438
|
+
return { leaves, dividers };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function countColumnsFromGeometry(geometry: DockLeafLayout[]): number {
|
|
442
|
+
if (geometry.length === 0) return 0;
|
|
443
|
+
const midY = geometry.reduce((sum, entry) => sum + entry.rect.y + Math.floor(entry.rect.height / 2), 0) / geometry.length;
|
|
444
|
+
return geometry.filter((entry) => midY >= entry.rect.y && midY < entry.rect.y + entry.rect.height).length;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function normalizeDropPosition(position: LeafDropPosition): "left" | "right" | "top" | "bottom" | "center" {
|
|
448
|
+
switch (position) {
|
|
449
|
+
case "top-left":
|
|
450
|
+
case "top-right":
|
|
451
|
+
return "top";
|
|
452
|
+
case "bottom-left":
|
|
453
|
+
case "bottom-right":
|
|
454
|
+
return "bottom";
|
|
455
|
+
default:
|
|
456
|
+
return position;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function buildGridDockTree(instanceIds: string[], depth = 0): DockLayoutNode | null {
|
|
461
|
+
if (instanceIds.length === 0) return null;
|
|
462
|
+
if (instanceIds.length === 1) return { kind: "pane", instanceId: instanceIds[0]! };
|
|
463
|
+
const splitIndex = Math.ceil(instanceIds.length / 2);
|
|
464
|
+
return {
|
|
465
|
+
kind: "split",
|
|
466
|
+
axis: depth % 2 === 0 ? "horizontal" : "vertical",
|
|
467
|
+
ratio: splitIndex / instanceIds.length,
|
|
468
|
+
first: buildGridDockTree(instanceIds.slice(0, splitIndex), depth + 1)!,
|
|
469
|
+
second: buildGridDockTree(instanceIds.slice(splitIndex), depth + 1)!,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
interface GridlockRect {
|
|
474
|
+
instanceId: string;
|
|
475
|
+
x: number;
|
|
476
|
+
y: number;
|
|
477
|
+
width: number;
|
|
478
|
+
height: number;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function boundsForRects(rects: GridlockRect[]): LayoutBounds {
|
|
482
|
+
const minX = Math.min(...rects.map((rect) => rect.x));
|
|
483
|
+
const minY = Math.min(...rects.map((rect) => rect.y));
|
|
484
|
+
const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));
|
|
485
|
+
const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));
|
|
486
|
+
return {
|
|
487
|
+
x: minX,
|
|
488
|
+
y: minY,
|
|
489
|
+
width: Math.max(1, maxX - minX),
|
|
490
|
+
height: Math.max(1, maxY - minY),
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function clampGridlockRatio(value: number): number {
|
|
495
|
+
if (!Number.isFinite(value)) return 0.5;
|
|
496
|
+
return Math.max(0.1, Math.min(0.9, value));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function inferSplitCandidate(
|
|
500
|
+
rects: GridlockRect[],
|
|
501
|
+
axis: "horizontal" | "vertical",
|
|
502
|
+
bounds: LayoutBounds,
|
|
503
|
+
): { axis: "horizontal" | "vertical"; ratio: number; first: GridlockRect[]; second: GridlockRect[] } | null {
|
|
504
|
+
const tolerance = 1;
|
|
505
|
+
const candidates = new Set<number>();
|
|
506
|
+
|
|
507
|
+
for (const rect of rects) {
|
|
508
|
+
if (axis === "horizontal") {
|
|
509
|
+
candidates.add(rect.y);
|
|
510
|
+
candidates.add(rect.y + rect.height);
|
|
511
|
+
} else {
|
|
512
|
+
candidates.add(rect.x);
|
|
513
|
+
candidates.add(rect.x + rect.width);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const minBound = axis === "horizontal" ? bounds.y : bounds.x;
|
|
518
|
+
const maxBound = axis === "horizontal" ? bounds.y + bounds.height : bounds.x + bounds.width;
|
|
519
|
+
|
|
520
|
+
let best: { ratio: number; first: GridlockRect[]; second: GridlockRect[]; score: number } | null = null;
|
|
521
|
+
for (const candidate of candidates) {
|
|
522
|
+
if (candidate <= minBound + tolerance || candidate >= maxBound - tolerance) continue;
|
|
523
|
+
|
|
524
|
+
const first: GridlockRect[] = [];
|
|
525
|
+
const second: GridlockRect[] = [];
|
|
526
|
+
let valid = true;
|
|
527
|
+
|
|
528
|
+
for (const rect of rects) {
|
|
529
|
+
const start = axis === "horizontal" ? rect.y : rect.x;
|
|
530
|
+
const end = axis === "horizontal" ? rect.y + rect.height : rect.x + rect.width;
|
|
531
|
+
|
|
532
|
+
if (end <= candidate + tolerance) {
|
|
533
|
+
first.push(rect);
|
|
534
|
+
} else if (start >= candidate - tolerance) {
|
|
535
|
+
second.push(rect);
|
|
536
|
+
} else {
|
|
537
|
+
valid = false;
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (!valid || first.length === 0 || second.length === 0) continue;
|
|
543
|
+
|
|
544
|
+
const ratio = axis === "horizontal"
|
|
545
|
+
? clampGridlockRatio((candidate - bounds.y) / Math.max(1, bounds.height))
|
|
546
|
+
: clampGridlockRatio((candidate - bounds.x) / Math.max(1, bounds.width));
|
|
547
|
+
const balance = Math.abs(first.length - second.length);
|
|
548
|
+
const centerBias = Math.abs(ratio - 0.5);
|
|
549
|
+
const score = balance * 10 + centerBias;
|
|
550
|
+
|
|
551
|
+
if (!best || score < best.score) {
|
|
552
|
+
best = { ratio, first, second, score };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return best ? { axis, ratio: best.ratio, first: best.first, second: best.second } : null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function inferDockTreeFromRects(rects: GridlockRect[], bounds?: LayoutBounds): DockLayoutNode | null {
|
|
560
|
+
if (rects.length === 0) return null;
|
|
561
|
+
if (rects.length === 1) return { kind: "pane", instanceId: rects[0]!.instanceId };
|
|
562
|
+
|
|
563
|
+
const rectBounds = bounds ?? boundsForRects(rects);
|
|
564
|
+
const preferredAxis = rectBounds.width >= rectBounds.height ? "vertical" : "horizontal";
|
|
565
|
+
const primary = inferSplitCandidate(rects, preferredAxis, rectBounds);
|
|
566
|
+
const secondary = inferSplitCandidate(rects, preferredAxis === "vertical" ? "horizontal" : "vertical", rectBounds);
|
|
567
|
+
const chosen = primary ?? secondary;
|
|
568
|
+
|
|
569
|
+
if (!chosen) {
|
|
570
|
+
return buildGridDockTree(
|
|
571
|
+
[...rects].sort((a, b) => a.y - b.y || a.x - b.x).map((rect) => rect.instanceId),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
kind: "split",
|
|
577
|
+
axis: chosen.axis === "vertical" ? "horizontal" : "vertical",
|
|
578
|
+
ratio: chosen.ratio,
|
|
579
|
+
first: inferDockTreeFromRects(chosen.first, boundsForRects(chosen.first))!,
|
|
580
|
+
second: inferDockTreeFromRects(chosen.second, boundsForRects(chosen.second))!,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function traverseDockLeaves(layout: LayoutConfig): DockLeafRef[] {
|
|
585
|
+
return collectDockLeafRefs(layout.dockRoot);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getDockedPaneIds(layout: LayoutConfig): string[] {
|
|
589
|
+
return getDockedInstanceIds(layout.dockRoot);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export function findDockLeaf(layout: LayoutConfig, instanceId: string): DockLeafRef | null {
|
|
593
|
+
return traverseDockLeaves(layout).find((entry) => entry.instanceId === instanceId) ?? null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function getDockLeafLayouts(layout: LayoutConfig, bounds: LayoutBounds): DockLeafLayout[] {
|
|
597
|
+
return collectDockGeometry(layout.dockRoot, bounds).leaves;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export function getDockDividerLayouts(layout: LayoutConfig, bounds: LayoutBounds): DockDividerLayout[] {
|
|
601
|
+
return collectDockGeometry(layout.dockRoot, bounds).dividers;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export function getLeafRect(layout: LayoutConfig, instanceId: string, bounds: LayoutBounds): LayoutBounds | null {
|
|
605
|
+
return getDockLeafLayouts(layout, bounds).find((entry) => entry.instanceId === instanceId)?.rect ?? null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function resolveDocked(
|
|
609
|
+
layout: LayoutConfig,
|
|
610
|
+
registeredPanes: ReadonlyMap<string, PaneDef>,
|
|
611
|
+
): ResolvedPane[] {
|
|
612
|
+
return traverseDockLeaves(layout)
|
|
613
|
+
.map((leaf) => {
|
|
614
|
+
const instance = findPaneInstance(layout, leaf.instanceId);
|
|
615
|
+
if (!instance) return null;
|
|
616
|
+
const def = registeredPanes.get(instance.paneId);
|
|
617
|
+
if (!def) return null;
|
|
618
|
+
return { instance, def, path: leaf.path };
|
|
619
|
+
})
|
|
620
|
+
.filter((pane): pane is ResolvedPane => pane !== null);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function resolveFloating(
|
|
624
|
+
layout: LayoutConfig,
|
|
625
|
+
registeredPanes: ReadonlyMap<string, PaneDef>,
|
|
626
|
+
): ResolvedPane[] {
|
|
627
|
+
const result: ResolvedPane[] = [];
|
|
628
|
+
for (const entry of layout.floating) {
|
|
629
|
+
const instance = findPaneInstance(layout, entry.instanceId);
|
|
630
|
+
if (!instance) continue;
|
|
631
|
+
const def = registeredPanes.get(instance.paneId);
|
|
632
|
+
if (!def) continue;
|
|
633
|
+
result.push({ instance, def, floating: entry });
|
|
634
|
+
}
|
|
635
|
+
result.sort((a, b) => (a.floating?.zIndex ?? 50) - (b.floating?.zIndex ?? 50));
|
|
636
|
+
return result;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function floatAtRect(layout: LayoutConfig, instanceId: string, rect: FloatingRect): LayoutConfig {
|
|
640
|
+
const base = detachPane(layout, instanceId);
|
|
641
|
+
return finalizeLayout({
|
|
642
|
+
...base,
|
|
643
|
+
floating: [
|
|
644
|
+
...base.floating.filter((entry) => entry.instanceId !== instanceId),
|
|
645
|
+
{
|
|
646
|
+
instanceId,
|
|
647
|
+
x: rect.x,
|
|
648
|
+
y: rect.y,
|
|
649
|
+
width: rect.width,
|
|
650
|
+
height: rect.height,
|
|
651
|
+
zIndex: rect.zIndex ?? maxFloatingZ(layout) + 1,
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function floatPane(
|
|
658
|
+
layout: LayoutConfig,
|
|
659
|
+
instanceId: string,
|
|
660
|
+
termWidth: number,
|
|
661
|
+
termHeight: number,
|
|
662
|
+
def?: PaneDef,
|
|
663
|
+
): LayoutConfig {
|
|
664
|
+
const instance = findPaneInstance(layout, instanceId);
|
|
665
|
+
if (!instance) return layout;
|
|
666
|
+
const remembered = instance.placementMemory?.floating;
|
|
667
|
+
const rect = remembered
|
|
668
|
+
? clampFloatingRect(remembered, termWidth, termHeight)
|
|
669
|
+
: defaultFloatingRect(termWidth, termHeight, def);
|
|
670
|
+
return floatAtRect(layout, instanceId, rect);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function insertRelativeToLeaf(layout: LayoutConfig, instanceId: string, targetId: string, position: DockTarget["position"]): LayoutConfig {
|
|
674
|
+
if (instanceId === targetId) return layout;
|
|
675
|
+
const base = detachPane(layout, instanceId);
|
|
676
|
+
const targetLeaf = findDockLeaf(base, targetId);
|
|
677
|
+
if (!targetLeaf || !base.dockRoot) return layout;
|
|
678
|
+
const targetNode = getNodeAtPath(base.dockRoot, targetLeaf.path);
|
|
679
|
+
if (!targetNode) return layout;
|
|
680
|
+
return finalizeLayout({
|
|
681
|
+
...base,
|
|
682
|
+
dockRoot: replaceNodeAtPath(base.dockRoot, targetLeaf.path, buildSplitAroundNode(targetNode, instanceId, position)),
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
export function insertAtRootEdge(layout: LayoutConfig, instanceId: string, edge: "left" | "right" | "top" | "bottom"): LayoutConfig {
|
|
687
|
+
const base = detachPane(layout, instanceId);
|
|
688
|
+
if (!base.dockRoot) {
|
|
689
|
+
return finalizeLayout({ ...base, dockRoot: { kind: "pane", instanceId } });
|
|
690
|
+
}
|
|
691
|
+
return finalizeLayout({
|
|
692
|
+
...base,
|
|
693
|
+
dockRoot: buildSplitAroundNode(base.dockRoot, instanceId, edge === "top" ? "above" : edge === "bottom" ? "below" : edge),
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function dockAtTarget(layout: LayoutConfig, instanceId: string, target: DockTarget): LayoutConfig {
|
|
698
|
+
return insertRelativeToLeaf(layout, instanceId, target.relativeTo, target.position);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function dockPane(layout: LayoutConfig, instanceId: string, target?: DockTarget): LayoutConfig {
|
|
702
|
+
if (target) return dockAtTarget(layout, instanceId, target);
|
|
703
|
+
|
|
704
|
+
const instance = findPaneInstance(layout, instanceId);
|
|
705
|
+
const dockedMemory = instance?.placementMemory?.docked;
|
|
706
|
+
if (dockedMemory?.position && dockedMemory.path && dockedMemory.path.length > 0) {
|
|
707
|
+
return insertRelativeToSubtreePath(layout, instanceId, dockedMemory.path.slice(0, -1), dockedMemory.position);
|
|
708
|
+
}
|
|
709
|
+
if (dockedMemory?.anchorInstanceId && dockedMemory.position && findDockLeaf(layout, dockedMemory.anchorInstanceId)) {
|
|
710
|
+
return insertRelativeToLeaf(layout, instanceId, dockedMemory.anchorInstanceId, dockedMemory.position);
|
|
711
|
+
}
|
|
712
|
+
if (!layout.dockRoot) {
|
|
713
|
+
return finalizeLayout({ ...detachPane(layout, instanceId), dockRoot: { kind: "pane", instanceId } });
|
|
714
|
+
}
|
|
715
|
+
return insertAtRootEdge(layout, instanceId, "right");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export function dockPaneToRegion(layout: LayoutConfig, instanceId: string, region: GlobalDockRegion): LayoutConfig {
|
|
719
|
+
switch (region) {
|
|
720
|
+
case "left":
|
|
721
|
+
return insertAtRootEdge(layout, instanceId, "left");
|
|
722
|
+
case "right":
|
|
723
|
+
return insertAtRootEdge(layout, instanceId, "right");
|
|
724
|
+
case "top":
|
|
725
|
+
case "top-left":
|
|
726
|
+
case "top-right":
|
|
727
|
+
return insertAtRootEdge(layout, instanceId, "top");
|
|
728
|
+
case "bottom":
|
|
729
|
+
case "bottom-left":
|
|
730
|
+
case "bottom-right":
|
|
731
|
+
return insertAtRootEdge(layout, instanceId, "bottom");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function insertIntoQuadrant(layout: LayoutConfig, instanceId: string, targetId: string, quadrant: Exclude<LeafDropPosition, "left" | "right" | "top" | "bottom" | "center">): LayoutConfig {
|
|
736
|
+
const normalized = normalizeDropPosition(quadrant);
|
|
737
|
+
return insertRelativeToLeaf(layout, instanceId, targetId, normalized === "top" ? "above" : normalized === "bottom" ? "below" : normalized);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function applyDrop(layout: LayoutConfig, draggedId: string, dropTarget: DropTarget): LayoutConfig {
|
|
741
|
+
if (dropTarget.kind === "frame") {
|
|
742
|
+
return insertAtRootEdge(layout, draggedId, dropTarget.edge);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const normalized = normalizeDropPosition(dropTarget.position);
|
|
746
|
+
if (normalized === "center") {
|
|
747
|
+
return swapPanes(layout, draggedId, dropTarget.targetId);
|
|
748
|
+
}
|
|
749
|
+
const direction = normalized === "top" ? "above" : normalized === "bottom" ? "below" : normalized;
|
|
750
|
+
return insertRelativeToLeaf(layout, draggedId, dropTarget.targetId, direction);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function simulateDrop(layout: LayoutConfig, draggedId: string, dropTarget: DropTarget, bounds: LayoutBounds): LayoutSimulation {
|
|
754
|
+
const nextLayout = applyDrop(layout, draggedId, dropTarget);
|
|
755
|
+
return {
|
|
756
|
+
layout: nextLayout,
|
|
757
|
+
previewRect: getLeafRect(nextLayout, draggedId, bounds),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function movePaneRelative(
|
|
762
|
+
layout: LayoutConfig,
|
|
763
|
+
instanceId: string,
|
|
764
|
+
position: DockTarget["position"],
|
|
765
|
+
): LayoutConfig {
|
|
766
|
+
const floating = layout.floating.find((entry) => entry.instanceId === instanceId);
|
|
767
|
+
if (floating) {
|
|
768
|
+
const deltaX = position === "left" ? -2 : position === "right" ? 2 : 0;
|
|
769
|
+
const deltaY = position === "above" ? -1 : position === "below" ? 1 : 0;
|
|
770
|
+
return finalizeLayout(updateFloatingPane(layout, instanceId, {
|
|
771
|
+
x: Math.max(0, floating.x + deltaX),
|
|
772
|
+
y: Math.max(0, floating.y + deltaY),
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const currentRect = getLeafRect(layout, instanceId, { x: 0, y: 0, width: 120, height: 40 });
|
|
777
|
+
if (!currentRect) return layout;
|
|
778
|
+
const candidates = getDockLeafLayouts(layout, { x: 0, y: 0, width: 120, height: 40 })
|
|
779
|
+
.filter((entry) => entry.instanceId !== instanceId)
|
|
780
|
+
.map((entry) => ({ entry, score: scoreDirectionalCandidate({ instanceId, path: [], rect: currentRect }, entry, position) }))
|
|
781
|
+
.filter((entry): entry is { entry: DockLeafLayout; score: number } => entry.score !== null)
|
|
782
|
+
.sort((a, b) => a.score - b.score);
|
|
783
|
+
const target = candidates[0]?.entry;
|
|
784
|
+
if (!target) return layout;
|
|
785
|
+
return insertRelativeToLeaf(layout, instanceId, target.instanceId, position);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export function swapLeaves(layout: LayoutConfig, firstId: string, secondId: string): LayoutConfig {
|
|
789
|
+
return swapPanes(layout, firstId, secondId);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function swapPanes(layout: LayoutConfig, firstId: string, secondId: string): LayoutConfig {
|
|
793
|
+
if (firstId === secondId) return layout;
|
|
794
|
+
const firstLeaf = findDockLeaf(layout, firstId);
|
|
795
|
+
const secondLeaf = findDockLeaf(layout, secondId);
|
|
796
|
+
const firstFloating = layout.floating.find((entry) => entry.instanceId === firstId);
|
|
797
|
+
const secondFloating = layout.floating.find((entry) => entry.instanceId === secondId);
|
|
798
|
+
if (!firstLeaf && !firstFloating) return layout;
|
|
799
|
+
if (!secondLeaf && !secondFloating) return layout;
|
|
800
|
+
|
|
801
|
+
if (firstLeaf && secondLeaf && layout.dockRoot) {
|
|
802
|
+
const firstNode = getNodeAtPath(layout.dockRoot, firstLeaf.path);
|
|
803
|
+
const secondNode = getNodeAtPath(layout.dockRoot, secondLeaf.path);
|
|
804
|
+
if (!firstNode || !secondNode || firstNode.kind !== "pane" || secondNode.kind !== "pane") return layout;
|
|
805
|
+
const withFirst = replaceNodeAtPath(layout.dockRoot, firstLeaf.path, { kind: "pane", instanceId: secondId });
|
|
806
|
+
const withSecond = replaceNodeAtPath(withFirst, secondLeaf.path, { kind: "pane", instanceId: firstId });
|
|
807
|
+
return finalizeLayout({ ...layout, dockRoot: withSecond });
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (firstFloating && secondFloating) {
|
|
811
|
+
return finalizeLayout({
|
|
812
|
+
...layout,
|
|
813
|
+
floating: layout.floating.map((entry) => {
|
|
814
|
+
if (entry.instanceId === firstId) return { ...secondFloating, instanceId: firstId };
|
|
815
|
+
if (entry.instanceId === secondId) return { ...firstFloating, instanceId: secondId };
|
|
816
|
+
return entry;
|
|
817
|
+
}),
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const dockedId = firstLeaf ? firstId : secondId;
|
|
822
|
+
const floatingId = firstFloating ? firstId : secondId;
|
|
823
|
+
const dockedLeaf = firstLeaf ?? secondLeaf!;
|
|
824
|
+
const floatingEntry = firstFloating ?? secondFloating!;
|
|
825
|
+
const base = layout.dockRoot && dockedLeaf
|
|
826
|
+
? replaceNodeAtPath(layout.dockRoot, dockedLeaf.path, { kind: "pane", instanceId: floatingId })
|
|
827
|
+
: layout.dockRoot;
|
|
828
|
+
|
|
829
|
+
return finalizeLayout({
|
|
830
|
+
...layout,
|
|
831
|
+
dockRoot: base,
|
|
832
|
+
floating: [
|
|
833
|
+
...layout.floating.filter((entry) => entry.instanceId !== floatingId),
|
|
834
|
+
{
|
|
835
|
+
instanceId: dockedId,
|
|
836
|
+
x: floatingEntry.x,
|
|
837
|
+
y: floatingEntry.y,
|
|
838
|
+
width: floatingEntry.width,
|
|
839
|
+
height: floatingEntry.height,
|
|
840
|
+
zIndex: floatingEntry.zIndex,
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function removeLeafAndCollapse(layout: LayoutConfig, instanceId: string): LayoutConfig {
|
|
847
|
+
return finalizeLayout(removeDockedLeaf(layout, instanceId));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export function resizeSplitAtPath(layout: LayoutConfig, path: Array<0 | 1>, ratio: number): LayoutConfig {
|
|
851
|
+
const target = getNodeAtPath(layout.dockRoot, path);
|
|
852
|
+
if (!target || target.kind !== "split" || !layout.dockRoot) return layout;
|
|
853
|
+
return finalizeLayout({
|
|
854
|
+
...layout,
|
|
855
|
+
dockRoot: replaceNodeAtPath(layout.dockRoot, path, {
|
|
856
|
+
...target,
|
|
857
|
+
ratio: clampRatio(ratio),
|
|
858
|
+
}),
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export function restorePlacementMemory(
|
|
863
|
+
layout: LayoutConfig,
|
|
864
|
+
instanceId: string,
|
|
865
|
+
termWidth: number,
|
|
866
|
+
termHeight: number,
|
|
867
|
+
def?: PaneDef,
|
|
868
|
+
): LayoutConfig {
|
|
869
|
+
const isFloating = layout.floating.some((entry) => entry.instanceId === instanceId);
|
|
870
|
+
if (isFloating) return dockPane(layout, instanceId);
|
|
871
|
+
return floatPane(layout, instanceId, termWidth, termHeight, def);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export function addPaneToLayout(layout: LayoutConfig, instance: PaneInstanceConfig, target: DockTarget): LayoutConfig {
|
|
875
|
+
const withInstance = ensurePaneInstance(layout, instance);
|
|
876
|
+
if (!withInstance.dockRoot) {
|
|
877
|
+
return finalizeLayout({
|
|
878
|
+
...withInstance,
|
|
879
|
+
dockRoot: { kind: "pane", instanceId: instance.instanceId },
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return dockAtTarget(withInstance, instance.instanceId, target);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export function addPaneFloating(
|
|
886
|
+
layout: LayoutConfig,
|
|
887
|
+
instance: PaneInstanceConfig | string,
|
|
888
|
+
termWidth: number,
|
|
889
|
+
termHeight: number,
|
|
890
|
+
def?: PaneDef,
|
|
891
|
+
): LayoutConfig {
|
|
892
|
+
const resolvedInstance = typeof instance === "string" ? createPaneInstance(instance) : instance;
|
|
893
|
+
const withInstance = ensurePaneInstance(layout, resolvedInstance);
|
|
894
|
+
return floatPane(withInstance, resolvedInstance.instanceId, termWidth, termHeight, def);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
export function removePane(layout: LayoutConfig, instanceId: string): LayoutConfig {
|
|
898
|
+
return finalizeLayout(removePaneInstances(
|
|
899
|
+
{
|
|
900
|
+
...layout,
|
|
901
|
+
dockRoot: removeDockedLeaf(layout, instanceId).dockRoot,
|
|
902
|
+
floating: layout.floating.filter((entry) => entry.instanceId !== instanceId),
|
|
903
|
+
},
|
|
904
|
+
[instanceId],
|
|
905
|
+
));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export function isPaneInLayout(layout: LayoutConfig, instanceId: string): boolean {
|
|
909
|
+
return !!findDockLeaf(layout, instanceId)
|
|
910
|
+
|| layout.floating.some((entry) => entry.instanceId === instanceId);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function isPaneDocked(layout: LayoutConfig, instanceId: string): boolean {
|
|
914
|
+
return !!findDockLeaf(layout, instanceId);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export function updateFloatingPane(
|
|
918
|
+
layout: LayoutConfig,
|
|
919
|
+
instanceId: string,
|
|
920
|
+
updates: Partial<Pick<FloatingPaneEntry, "x" | "y" | "width" | "height" | "zIndex">>,
|
|
921
|
+
): LayoutConfig {
|
|
922
|
+
return {
|
|
923
|
+
...layout,
|
|
924
|
+
floating: layout.floating.map((entry) => (
|
|
925
|
+
entry.instanceId === instanceId ? { ...entry, ...updates } : entry
|
|
926
|
+
)),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export function bringToFront(layout: LayoutConfig, instanceId: string): LayoutConfig {
|
|
931
|
+
return finalizeLayout(updateFloatingPane(layout, instanceId, { zIndex: maxFloatingZ(layout) + 1 }));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function getLayoutPreview(layout: LayoutConfig): string {
|
|
935
|
+
const geometry = getDockLeafLayouts(layout, { x: 0, y: 0, width: 120, height: 40 });
|
|
936
|
+
return `${countColumnsFromGeometry(geometry)}c / ${geometry.length}d / ${layout.floating.length}f`;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function gridlockAllPanes(
|
|
940
|
+
layout: LayoutConfig,
|
|
941
|
+
bounds: LayoutBounds = { x: 0, y: 0, width: 120, height: 40 },
|
|
942
|
+
): LayoutConfig {
|
|
943
|
+
const dockedRects: GridlockRect[] = getDockLeafLayouts(layout, bounds)
|
|
944
|
+
.map((leaf) => ({ instanceId: leaf.instanceId, ...leaf.rect }));
|
|
945
|
+
const floatingRects: GridlockRect[] = layout.floating.map((entry) => ({
|
|
946
|
+
instanceId: entry.instanceId,
|
|
947
|
+
x: entry.x,
|
|
948
|
+
y: entry.y,
|
|
949
|
+
width: entry.width,
|
|
950
|
+
height: entry.height,
|
|
951
|
+
}));
|
|
952
|
+
const allRects = [...dockedRects, ...floatingRects];
|
|
953
|
+
if (allRects.length === 0) return layout;
|
|
954
|
+
|
|
955
|
+
return finalizeLayout({
|
|
956
|
+
...layout,
|
|
957
|
+
dockRoot: inferDockTreeFromRects(allRects, boundsForRects(allRects)),
|
|
958
|
+
floating: [],
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export function getRememberedFloatingRect(
|
|
963
|
+
layout: LayoutConfig,
|
|
964
|
+
instanceId: string,
|
|
965
|
+
termWidth: number,
|
|
966
|
+
termHeight: number,
|
|
967
|
+
def?: PaneDef,
|
|
968
|
+
): FloatingPlacementMemory {
|
|
969
|
+
const instance = findPaneInstance(layout, instanceId);
|
|
970
|
+
const remembered = instance?.placementMemory?.floating;
|
|
971
|
+
return remembered
|
|
972
|
+
? clampFloatingRect(remembered, termWidth, termHeight)
|
|
973
|
+
: defaultFloatingRect(termWidth, termHeight, def);
|
|
974
|
+
}
|