@flyingrobots/bijou-tui 3.1.0 → 4.1.0
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/LICENSE +159 -21
- package/README.md +205 -39
- package/dist/app-frame-actions.d.ts +5 -5
- package/dist/app-frame-actions.d.ts.map +1 -1
- package/dist/app-frame-actions.js +74 -9
- package/dist/app-frame-actions.js.map +1 -1
- package/dist/app-frame-i18n.d.ts +12 -0
- package/dist/app-frame-i18n.d.ts.map +1 -0
- package/dist/app-frame-i18n.js +92 -0
- package/dist/app-frame-i18n.js.map +1 -0
- package/dist/app-frame-layers.d.ts +29 -0
- package/dist/app-frame-layers.d.ts.map +1 -0
- package/dist/app-frame-layers.js +104 -0
- package/dist/app-frame-layers.js.map +1 -0
- package/dist/app-frame-palette.d.ts +6 -2
- package/dist/app-frame-palette.d.ts.map +1 -1
- package/dist/app-frame-palette.js +42 -10
- package/dist/app-frame-palette.js.map +1 -1
- package/dist/app-frame-render.d.ts +28 -14
- package/dist/app-frame-render.d.ts.map +1 -1
- package/dist/app-frame-render.js +285 -138
- package/dist/app-frame-render.js.map +1 -1
- package/dist/app-frame-types.d.ts +41 -21
- package/dist/app-frame-types.d.ts.map +1 -1
- package/dist/app-frame-types.js +8 -6
- package/dist/app-frame-types.js.map +1 -1
- package/dist/app-frame-utils.d.ts +8 -3
- package/dist/app-frame-utils.d.ts.map +1 -1
- package/dist/app-frame-utils.js +42 -27
- package/dist/app-frame-utils.js.map +1 -1
- package/dist/app-frame.d.ts +128 -12
- package/dist/app-frame.d.ts.map +1 -1
- package/dist/app-frame.js +1212 -91
- package/dist/app-frame.js.map +1 -1
- package/dist/browsable-list.d.ts +20 -1
- package/dist/browsable-list.d.ts.map +1 -1
- package/dist/browsable-list.js +48 -10
- package/dist/browsable-list.js.map +1 -1
- package/dist/collection-surface.d.ts +8 -0
- package/dist/collection-surface.d.ts.map +1 -0
- package/dist/collection-surface.js +41 -0
- package/dist/collection-surface.js.map +1 -0
- package/dist/command-palette.d.ts +17 -1
- package/dist/command-palette.d.ts.map +1 -1
- package/dist/command-palette.js +50 -20
- package/dist/command-palette.js.map +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/css/text-style.d.ts +2 -1
- package/dist/css/text-style.d.ts.map +1 -1
- package/dist/css/text-style.js +33 -0
- package/dist/css/text-style.js.map +1 -1
- package/dist/design-language.d.ts +49 -0
- package/dist/design-language.d.ts.map +1 -0
- package/dist/design-language.js +70 -0
- package/dist/design-language.js.map +1 -0
- package/dist/driver.d.ts +6 -1
- package/dist/driver.d.ts.map +1 -1
- package/dist/driver.js +3 -0
- package/dist/driver.js.map +1 -1
- package/dist/eventbus.d.ts.map +1 -1
- package/dist/eventbus.js +21 -1
- package/dist/eventbus.js.map +1 -1
- package/dist/file-picker.d.ts +19 -1
- package/dist/file-picker.d.ts.map +1 -1
- package/dist/file-picker.js +47 -20
- package/dist/file-picker.js.map +1 -1
- package/dist/flex.d.ts +35 -1
- package/dist/flex.d.ts.map +1 -1
- package/dist/flex.js +127 -1
- package/dist/flex.js.map +1 -1
- package/dist/focus-area.d.ts +20 -1
- package/dist/focus-area.d.ts.map +1 -1
- package/dist/focus-area.js +107 -12
- package/dist/focus-area.js.map +1 -1
- package/dist/grid.d.ts +10 -0
- package/dist/grid.d.ts.map +1 -1
- package/dist/grid.js +25 -0
- package/dist/grid.js.map +1 -1
- package/dist/help.d.ts +41 -0
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +50 -0
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +23 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -16
- package/dist/index.js.map +1 -1
- package/dist/inspector-drawer.d.ts +36 -0
- package/dist/inspector-drawer.d.ts.map +1 -0
- package/dist/inspector-drawer.js +68 -0
- package/dist/inspector-drawer.js.map +1 -0
- package/dist/layout-node-surface.d.ts +17 -0
- package/dist/layout-node-surface.d.ts.map +1 -0
- package/dist/layout-node-surface.js +61 -0
- package/dist/layout-node-surface.js.map +1 -0
- package/dist/navigable-table.d.ts +16 -1
- package/dist/navigable-table.d.ts.map +1 -1
- package/dist/navigable-table.js +32 -1
- package/dist/navigable-table.js.map +1 -1
- package/dist/notification.d.ts +26 -1
- package/dist/notification.d.ts.map +1 -1
- package/dist/notification.js +294 -41
- package/dist/notification.js.map +1 -1
- package/dist/overlay.d.ts +29 -8
- package/dist/overlay.d.ts.map +1 -1
- package/dist/overlay.js +248 -137
- package/dist/overlay.js.map +1 -1
- package/dist/pager.d.ts +16 -0
- package/dist/pager.d.ts.map +1 -1
- package/dist/pager.js +61 -1
- package/dist/pager.js.map +1 -1
- package/dist/runtime-engine.d.ts +223 -0
- package/dist/runtime-engine.d.ts.map +1 -0
- package/dist/runtime-engine.js +457 -0
- package/dist/runtime-engine.js.map +1 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +33 -19
- package/dist/runtime.js.map +1 -1
- package/dist/shell-quit.d.ts +12 -0
- package/dist/shell-quit.d.ts.map +1 -0
- package/dist/shell-quit.js +36 -0
- package/dist/shell-quit.js.map +1 -0
- package/dist/split-pane.d.ts +12 -1
- package/dist/split-pane.d.ts.map +1 -1
- package/dist/split-pane.js +31 -1
- package/dist/split-pane.js.map +1 -1
- package/dist/status-bar.d.ts +12 -0
- package/dist/status-bar.d.ts.map +1 -1
- package/dist/status-bar.js +45 -16
- package/dist/status-bar.js.map +1 -1
- package/dist/subapp/mount.d.ts.map +1 -1
- package/dist/subapp/mount.js +3 -0
- package/dist/subapp/mount.js.map +1 -1
- package/dist/surface-layout.d.ts +19 -0
- package/dist/surface-layout.d.ts.map +1 -0
- package/dist/surface-layout.js +87 -0
- package/dist/surface-layout.js.map +1 -0
- package/dist/transition-shaders.d.ts +10 -8
- package/dist/transition-shaders.d.ts.map +1 -1
- package/dist/transition-shaders.js +65 -19
- package/dist/transition-shaders.js.map +1 -1
- package/dist/types.d.ts +21 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -1
- package/dist/view-output.d.ts +5 -4
- package/dist/view-output.d.ts.map +1 -1
- package/dist/view-output.js +37 -29
- package/dist/view-output.js.map +1 -1
- package/dist/viewport.d.ts +30 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +77 -1
- package/dist/viewport.js.map +1 -1
- package/package.json +6 -3
- package/dist/layout-v3.d.ts +0 -10
- package/dist/layout-v3.d.ts.map +0 -1
- package/dist/layout-v3.js +0 -35
- package/dist/layout-v3.js.map +0 -1
package/dist/app-frame-render.js
CHANGED
|
@@ -2,56 +2,160 @@
|
|
|
2
2
|
* Layout rendering for `app-frame.ts`.
|
|
3
3
|
*
|
|
4
4
|
* Recursive tree renderer, page content, maximized pane, header/help lines,
|
|
5
|
-
* transition shader, and string
|
|
5
|
+
* transition shader, and surface/string bridge helpers.
|
|
6
6
|
*/
|
|
7
|
-
import { resolveSafeCtx,
|
|
7
|
+
import { createSurface, darken, hexToRgb, lighten, mix, parseAnsiToSurface, saturate, resolveSafeCtx, } from '@flyingrobots/bijou';
|
|
8
8
|
import { TRANSITION_SHADERS } from './transition-shaders.js';
|
|
9
9
|
import { fitBlock } from './layout-utils.js';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { splitPaneLayout } from './split-pane.js';
|
|
11
|
+
import { gridLayout } from './grid.js';
|
|
12
|
+
import { createFocusAreaStateForSurface, focusAreaSurfaceInto, focusAreaScrollTo, focusAreaScrollToX, } from './focus-area.js';
|
|
13
13
|
import { isMinimized, createPanelVisibilityState } from './panel-state.js';
|
|
14
14
|
import { createPanelDockState, resolveChildOrder, getNodeId } from './panel-dock.js';
|
|
15
|
-
import {
|
|
15
|
+
import { visibleLength } from './viewport.js';
|
|
16
16
|
import { helpShort } from './help.js';
|
|
17
|
-
import { findPaneNode, isPaneMinimized, mergeMaps, offsetRect, fitLine,
|
|
18
|
-
import { normalizeViewOutput } from './view-output.js';
|
|
19
|
-
import {
|
|
17
|
+
import { findPaneNode, isPaneMinimized, mergeMaps, offsetRect, fitLine, } from './app-frame-utils.js';
|
|
18
|
+
import { normalizeViewOutput, normalizeViewOutputInto } from './view-output.js';
|
|
19
|
+
import { createStyledTextSurfaceWithBCSS } from './css/text-style.js';
|
|
20
|
+
import { frameModeLabel } from './app-frame-i18n.js';
|
|
21
|
+
const framePaneScratchBySize = new Map();
|
|
22
|
+
function relativeLuminance(hex) {
|
|
23
|
+
const [red, green, blue] = hexToRgb(hex);
|
|
24
|
+
const r = srgbChannelToLinear(red);
|
|
25
|
+
const g = srgbChannelToLinear(green);
|
|
26
|
+
const b = srgbChannelToLinear(blue);
|
|
27
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
28
|
+
}
|
|
29
|
+
function srgbChannelToLinear(channel) {
|
|
30
|
+
const normalized = channel / 255;
|
|
31
|
+
return normalized <= 0.03928
|
|
32
|
+
? normalized / 12.92
|
|
33
|
+
: ((normalized + 0.055) / 1.055) ** 2.4;
|
|
34
|
+
}
|
|
35
|
+
function contrastRatio(a, b) {
|
|
36
|
+
const lighter = Math.max(relativeLuminance(a), relativeLuminance(b));
|
|
37
|
+
const darker = Math.min(relativeLuminance(a), relativeLuminance(b));
|
|
38
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
39
|
+
}
|
|
40
|
+
function colorDistance(a, b) {
|
|
41
|
+
const [ar, ag, ab] = hexToRgb(a);
|
|
42
|
+
const [br, bg, bb] = hexToRgb(b);
|
|
43
|
+
return Math.sqrt((ar - br) ** 2 + (ag - bg) ** 2 + (ab - bb) ** 2);
|
|
44
|
+
}
|
|
45
|
+
function deriveActiveHeaderTabToken(ctx, backgroundHex, baseHex) {
|
|
46
|
+
const darkBackground = relativeLuminance(backgroundHex) < 0.35;
|
|
47
|
+
const accent = ctx.semantic('accent');
|
|
48
|
+
const info = ctx.semantic('info');
|
|
49
|
+
const primary = ctx.semantic('primary');
|
|
50
|
+
const warning = ctx.semantic('warning');
|
|
51
|
+
const seeds = [
|
|
52
|
+
accent,
|
|
53
|
+
info,
|
|
54
|
+
mix(accent, info, 0.5),
|
|
55
|
+
mix(accent, warning, 0.3),
|
|
56
|
+
mix(primary, accent, 0.3),
|
|
57
|
+
];
|
|
58
|
+
const candidates = seeds.flatMap((seed) => {
|
|
59
|
+
const emphasized = saturate(seed, 0.35);
|
|
60
|
+
return darkBackground
|
|
61
|
+
? [
|
|
62
|
+
emphasized,
|
|
63
|
+
lighten(seed, 0.18),
|
|
64
|
+
lighten(emphasized, 0.3),
|
|
65
|
+
lighten(mix(seed, primary, 0.25), 0.2),
|
|
66
|
+
]
|
|
67
|
+
: [
|
|
68
|
+
darken(seed, 0.18),
|
|
69
|
+
darken(emphasized, 0.28),
|
|
70
|
+
darken(mix(seed, primary, 0.1), 0.22),
|
|
71
|
+
];
|
|
72
|
+
});
|
|
73
|
+
let best = candidates[0] ?? accent;
|
|
74
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
const contrast = contrastRatio(candidate.hex, backgroundHex);
|
|
77
|
+
const distance = colorDistance(candidate.hex, baseHex) / Math.sqrt(3 * 255 * 255);
|
|
78
|
+
const score = contrast * 3 + distance;
|
|
79
|
+
if (score > bestScore) {
|
|
80
|
+
bestScore = score;
|
|
81
|
+
best = candidate;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
hex: best.hex,
|
|
86
|
+
modifiers: ['bold'],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function paintActiveHeaderTab(surface, tabTargets, activePageId, ctx, tokenOverride) {
|
|
90
|
+
if (ctx == null)
|
|
91
|
+
return;
|
|
92
|
+
const activeTarget = tabTargets.find((target) => target.pageId === activePageId);
|
|
93
|
+
if (activeTarget == null)
|
|
94
|
+
return;
|
|
95
|
+
const sampleCell = surface.get(activeTarget.startCol, 0);
|
|
96
|
+
const backgroundHex = sampleCell.bg
|
|
97
|
+
?? ctx.surface('primary').bg
|
|
98
|
+
?? ctx.surface('secondary').bg
|
|
99
|
+
?? '#000000';
|
|
100
|
+
const baseHex = sampleCell.fg
|
|
101
|
+
?? ctx.surface('primary').hex
|
|
102
|
+
?? ctx.semantic('primary').hex;
|
|
103
|
+
const token = tokenOverride ?? deriveActiveHeaderTabToken(ctx, backgroundHex, baseHex);
|
|
104
|
+
for (let x = activeTarget.startCol; x <= activeTarget.endCol; x++) {
|
|
105
|
+
const cell = surface.get(x, 0);
|
|
106
|
+
surface.set(x, 0, {
|
|
107
|
+
...cell,
|
|
108
|
+
fg: token.hex,
|
|
109
|
+
bg: token.bg ?? cell.bg,
|
|
110
|
+
modifiers: token.modifiers == null
|
|
111
|
+
? cell.modifiers
|
|
112
|
+
: Array.from(new Set([...(cell.modifiers ?? []), ...token.modifiers])),
|
|
113
|
+
empty: false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
20
117
|
/** Recursively render a layout tree node (pane, split, or grid) into a rect. */
|
|
21
118
|
export function renderFrameNode(node, rect, ctx) {
|
|
22
119
|
if (rect.width <= 0 || rect.height <= 0) {
|
|
23
|
-
return {
|
|
120
|
+
return { surface: createSurface(rect.width, rect.height), paneRects: new Map(), paneOrder: [] };
|
|
121
|
+
}
|
|
122
|
+
const surface = createSurface(rect.width, rect.height);
|
|
123
|
+
const painted = paintFrameNodeInto(node, { row: 0, col: 0, width: rect.width, height: rect.height }, rect, ctx, surface);
|
|
124
|
+
return { surface, paneRects: painted.paneRects, paneOrder: painted.paneOrder };
|
|
125
|
+
}
|
|
126
|
+
function paintFrameNodeInto(node, localRect, absoluteRect, ctx, target) {
|
|
127
|
+
if (localRect.width <= 0 || localRect.height <= 0) {
|
|
128
|
+
return { paneRects: new Map(), paneOrder: [] };
|
|
24
129
|
}
|
|
25
130
|
if (node.kind === 'pane') {
|
|
26
131
|
// Minimized pane: render as collapsed title bar
|
|
27
132
|
if (isMinimized(ctx.visibility, node.paneId)) {
|
|
28
133
|
const titleBar = `[${node.paneId}] \u25b8`; // ▸
|
|
29
|
-
|
|
134
|
+
target.blit(blockSurface(titleBar, localRect.width, localRect.height), localRect.col, localRect.row);
|
|
30
135
|
return {
|
|
31
|
-
|
|
32
|
-
paneRects: new Map([[node.paneId, rect]]),
|
|
136
|
+
paneRects: new Map([[node.paneId, absoluteRect]]),
|
|
33
137
|
paneOrder: [node.paneId],
|
|
34
138
|
};
|
|
35
139
|
}
|
|
36
140
|
const prior = ctx.scrollByPane[node.paneId] ?? { x: 0, y: 0 };
|
|
37
|
-
const
|
|
38
|
-
let state =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
height: rect.height,
|
|
141
|
+
const contentSurface = framePaneOutputToSurface(node.render(localRect.width, localRect.height), localRect.width, localRect.height, getFramePaneScratch(localRect.width, localRect.height));
|
|
142
|
+
let state = createFocusAreaStateForSurface(contentSurface, {
|
|
143
|
+
width: localRect.width,
|
|
144
|
+
height: localRect.height,
|
|
42
145
|
overflowX: node.overflowX ?? 'hidden',
|
|
43
146
|
});
|
|
44
147
|
state = focusAreaScrollTo(state, prior.y);
|
|
45
148
|
state = focusAreaScrollToX(state, prior.x);
|
|
46
|
-
|
|
149
|
+
focusAreaSurfaceInto(contentSurface, state, target, {
|
|
47
150
|
focused: node.paneId === ctx.focusedPaneId,
|
|
48
151
|
ctx: resolveSafeCtx(),
|
|
49
152
|
id: node.paneId,
|
|
50
153
|
classes: [node.paneId === ctx.focusedPaneId ? 'focused' : 'unfocused'],
|
|
51
|
-
|
|
154
|
+
focusedGutterToken: node.focusedGutterToken,
|
|
155
|
+
unfocusedGutterToken: node.unfocusedGutterToken,
|
|
156
|
+
}, localRect.col, localRect.row);
|
|
52
157
|
return {
|
|
53
|
-
|
|
54
|
-
paneRects: new Map([[node.paneId, rect]]),
|
|
158
|
+
paneRects: new Map([[node.paneId, absoluteRect]]),
|
|
55
159
|
paneOrder: [node.paneId],
|
|
56
160
|
};
|
|
57
161
|
}
|
|
@@ -74,75 +178,56 @@ export function renderFrameNode(node, rect, ctx) {
|
|
|
74
178
|
}
|
|
75
179
|
if (aMinimized && !bMinimized) {
|
|
76
180
|
// A is minimized: give it minimal space
|
|
77
|
-
const mainAxisTotal = direction === 'row' ?
|
|
181
|
+
const mainAxisTotal = direction === 'row' ? localRect.width : localRect.height;
|
|
78
182
|
const minimizedSize = Math.min(1, mainAxisTotal);
|
|
79
183
|
splitState = { ...splitState, ratio: minimizedSize / Math.max(1, mainAxisTotal) };
|
|
80
184
|
}
|
|
81
185
|
else if (bMinimized && !aMinimized) {
|
|
82
186
|
// B is minimized: give A most of the space
|
|
83
|
-
const mainAxisTotal = direction === 'row' ?
|
|
187
|
+
const mainAxisTotal = direction === 'row' ? localRect.width : localRect.height;
|
|
84
188
|
const minimizedSize = Math.min(1, mainAxisTotal);
|
|
85
189
|
splitState = { ...splitState, ratio: 1 - minimizedSize / Math.max(1, mainAxisTotal) };
|
|
86
190
|
}
|
|
87
191
|
const layout = splitPaneLayout(splitState, {
|
|
88
192
|
direction,
|
|
89
|
-
width:
|
|
90
|
-
height:
|
|
91
|
-
minA: node.minA,
|
|
92
|
-
minB: node.minB,
|
|
93
|
-
});
|
|
94
|
-
const aRect = offsetRect(layout.paneA, rect.row, rect.col);
|
|
95
|
-
const bRect = offsetRect(layout.paneB, rect.row, rect.col);
|
|
96
|
-
const a = renderFrameNode(effectiveA, aRect, ctx);
|
|
97
|
-
const b = renderFrameNode(effectiveB, bRect, ctx);
|
|
98
|
-
const output = splitPane(splitState, {
|
|
99
|
-
direction,
|
|
100
|
-
width: rect.width,
|
|
101
|
-
height: rect.height,
|
|
193
|
+
width: localRect.width,
|
|
194
|
+
height: localRect.height,
|
|
102
195
|
minA: node.minA,
|
|
103
196
|
minB: node.minB,
|
|
104
|
-
dividerChar: node.dividerChar,
|
|
105
|
-
paneA: () => a.output,
|
|
106
|
-
paneB: () => b.output,
|
|
107
197
|
});
|
|
198
|
+
const localARect = offsetRect(layout.paneA, localRect.row, localRect.col);
|
|
199
|
+
const localBRect = offsetRect(layout.paneB, localRect.row, localRect.col);
|
|
200
|
+
const absoluteARect = offsetRect(layout.paneA, absoluteRect.row, absoluteRect.col);
|
|
201
|
+
const absoluteBRect = offsetRect(layout.paneB, absoluteRect.row, absoluteRect.col);
|
|
202
|
+
const a = paintFrameNodeInto(effectiveA, localARect, absoluteARect, ctx, target);
|
|
203
|
+
const b = paintFrameNodeInto(effectiveB, localBRect, absoluteBRect, ctx, target);
|
|
204
|
+
paintDivider(target, offsetRect(layout.divider, localRect.row, localRect.col), node.dividerChar, direction);
|
|
108
205
|
return {
|
|
109
|
-
output,
|
|
110
206
|
paneRects: mergeMaps(a.paneRects, b.paneRects),
|
|
111
207
|
paneOrder: [...a.paneOrder, ...b.paneOrder],
|
|
112
208
|
};
|
|
113
209
|
}
|
|
114
210
|
const relRects = gridLayout({
|
|
115
|
-
width:
|
|
116
|
-
height:
|
|
211
|
+
width: localRect.width,
|
|
212
|
+
height: localRect.height,
|
|
117
213
|
columns: node.columns,
|
|
118
214
|
rows: node.rows,
|
|
119
215
|
areas: node.areas,
|
|
120
216
|
gap: node.gap,
|
|
121
217
|
});
|
|
122
|
-
|
|
218
|
+
let paneRects = new Map();
|
|
219
|
+
const seenPaneIds = new Set();
|
|
220
|
+
const paneOrder = [];
|
|
123
221
|
for (const [areaName, areaRect] of relRects) {
|
|
124
|
-
const
|
|
222
|
+
const localAreaRect = offsetRect(areaRect, localRect.row, localRect.col);
|
|
223
|
+
const absoluteAreaRect = offsetRect(areaRect, absoluteRect.row, absoluteRect.col);
|
|
125
224
|
const child = node.cells[areaName];
|
|
126
225
|
if (child == null) {
|
|
127
226
|
resolveSafeCtx()?.io.writeError(`createFramedApp: grid cell "${areaName}" missing in page "${ctx.pageId}" — rendering placeholder\n`);
|
|
128
|
-
|
|
227
|
+
target.blit(blockSurface(`[missing grid cell: ${areaName}]`, localAreaRect.width, localAreaRect.height), localAreaRect.col, localAreaRect.row);
|
|
129
228
|
continue;
|
|
130
229
|
}
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
const output = grid({
|
|
134
|
-
width: rect.width,
|
|
135
|
-
height: rect.height,
|
|
136
|
-
columns: node.columns,
|
|
137
|
-
rows: node.rows,
|
|
138
|
-
areas: node.areas,
|
|
139
|
-
gap: node.gap,
|
|
140
|
-
cells: Object.fromEntries([...renderedByArea.entries()].map(([name, rendered]) => [name, () => rendered.output])),
|
|
141
|
-
});
|
|
142
|
-
let paneRects = new Map();
|
|
143
|
-
const seenPaneIds = new Set();
|
|
144
|
-
const paneOrder = [];
|
|
145
|
-
for (const rendered of renderedByArea.values()) {
|
|
230
|
+
const rendered = paintFrameNodeInto(child, localAreaRect, absoluteAreaRect, ctx, target);
|
|
146
231
|
for (const [paneId, paneRect] of rendered.paneRects.entries()) {
|
|
147
232
|
if (paneRects.has(paneId)) {
|
|
148
233
|
throw new Error(`createFramedApp: duplicate paneId "${paneId}" in rendered layout`);
|
|
@@ -157,18 +242,24 @@ export function renderFrameNode(node, rect, ctx) {
|
|
|
157
242
|
paneOrder.push(paneId);
|
|
158
243
|
}
|
|
159
244
|
}
|
|
160
|
-
return {
|
|
245
|
+
return { paneRects, paneOrder };
|
|
161
246
|
}
|
|
162
247
|
/** Render a placeholder for a grid area that has no matching cell definition. */
|
|
163
248
|
export function renderMissingGridCell(areaName, rect) {
|
|
164
249
|
return {
|
|
165
|
-
|
|
250
|
+
surface: blockSurface(`[missing grid cell: ${areaName}]`, rect.width, rect.height),
|
|
166
251
|
paneRects: new Map(),
|
|
167
252
|
paneOrder: [],
|
|
168
253
|
};
|
|
169
254
|
}
|
|
170
255
|
/** Render a page's layout tree within the frame body rect. */
|
|
171
256
|
export function renderPageContent(pageId, model, bodyRect, pagesById) {
|
|
257
|
+
const surface = createSurface(bodyRect.width, bodyRect.height);
|
|
258
|
+
const geometry = renderPageContentInto(pageId, model, bodyRect, pagesById, surface, 0, 0);
|
|
259
|
+
return { surface, paneRects: geometry.paneRects, paneOrder: geometry.paneOrder };
|
|
260
|
+
}
|
|
261
|
+
/** Paint a page's layout tree directly into an existing target surface. */
|
|
262
|
+
export function renderPageContentInto(pageId, model, bodyRect, pagesById, target, offsetRow = bodyRect.row, offsetCol = bodyRect.col) {
|
|
172
263
|
const page = pagesById.get(pageId);
|
|
173
264
|
const pageModel = model.pageModels[pageId];
|
|
174
265
|
const renderCtx = {
|
|
@@ -179,92 +270,131 @@ export function renderPageContent(pageId, model, bodyRect, pagesById) {
|
|
|
179
270
|
visibility: model.minimizedByPage[pageId] ?? createPanelVisibilityState(),
|
|
180
271
|
dockState: model.dockStateByPage[pageId] ?? createPanelDockState(),
|
|
181
272
|
};
|
|
182
|
-
return
|
|
273
|
+
return paintFrameNodeInto(page.layout(pageModel), { row: offsetRow, col: offsetCol, width: bodyRect.width, height: bodyRect.height }, bodyRect, renderCtx, target);
|
|
183
274
|
}
|
|
184
275
|
/** Render only the maximized pane at the full body rect. */
|
|
185
276
|
export function renderMaximizedPane(pageId, model, bodyRect, pagesById, maximizedPaneId) {
|
|
277
|
+
const surface = createSurface(bodyRect.width, bodyRect.height);
|
|
278
|
+
const geometry = renderMaximizedPaneInto(pageId, model, bodyRect, pagesById, maximizedPaneId, surface, 0, 0);
|
|
279
|
+
return { surface, paneRects: geometry.paneRects, paneOrder: geometry.paneOrder };
|
|
280
|
+
}
|
|
281
|
+
/** Paint only the maximized pane directly into an existing target surface. */
|
|
282
|
+
export function renderMaximizedPaneInto(pageId, model, bodyRect, pagesById, maximizedPaneId, target, offsetRow = bodyRect.row, offsetCol = bodyRect.col) {
|
|
186
283
|
const page = pagesById.get(pageId);
|
|
187
284
|
const pageModel = model.pageModels[pageId];
|
|
188
285
|
const layoutTree = page.layout(pageModel);
|
|
189
286
|
const paneNode = findPaneNode(layoutTree, maximizedPaneId);
|
|
190
287
|
if (paneNode == null) {
|
|
191
288
|
// Pane not found, fall back to normal rendering
|
|
192
|
-
return
|
|
289
|
+
return renderPageContentInto(pageId, model, bodyRect, pagesById, target, offsetRow, offsetCol);
|
|
193
290
|
}
|
|
194
291
|
const prior = model.scrollByPage[pageId]?.[maximizedPaneId] ?? { x: 0, y: 0 };
|
|
195
|
-
const
|
|
196
|
-
let state =
|
|
197
|
-
content,
|
|
292
|
+
const contentSurface = framePaneOutputToSurface(paneNode.render(bodyRect.width, bodyRect.height), bodyRect.width, bodyRect.height, getFramePaneScratch(bodyRect.width, bodyRect.height));
|
|
293
|
+
let state = createFocusAreaStateForSurface(contentSurface, {
|
|
198
294
|
width: bodyRect.width,
|
|
199
295
|
height: bodyRect.height,
|
|
200
296
|
overflowX: paneNode.overflowX ?? 'hidden',
|
|
201
297
|
});
|
|
202
298
|
state = focusAreaScrollTo(state, prior.y);
|
|
203
299
|
state = focusAreaScrollToX(state, prior.x);
|
|
204
|
-
|
|
300
|
+
focusAreaSurfaceInto(contentSurface, state, target, {
|
|
205
301
|
focused: true,
|
|
206
302
|
ctx: resolveSafeCtx(),
|
|
207
303
|
id: maximizedPaneId,
|
|
208
304
|
classes: ['focused', 'maximized'],
|
|
209
|
-
|
|
305
|
+
focusedGutterToken: paneNode.focusedGutterToken,
|
|
306
|
+
unfocusedGutterToken: paneNode.unfocusedGutterToken,
|
|
307
|
+
}, offsetCol, offsetRow);
|
|
210
308
|
return {
|
|
211
|
-
output,
|
|
212
309
|
paneRects: new Map([[maximizedPaneId, bodyRect]]),
|
|
213
310
|
paneOrder: [maximizedPaneId],
|
|
214
311
|
};
|
|
215
312
|
}
|
|
216
|
-
/**
|
|
217
|
-
export function
|
|
313
|
+
/** Resolve the top header line plus clickable tab target geometry. */
|
|
314
|
+
export function resolveHeaderLine(model, options, pagesById) {
|
|
315
|
+
const ctx = resolveSafeCtx();
|
|
316
|
+
const activePage = pagesById.get(model.activePageId);
|
|
317
|
+
const activePageModel = model.pageModels[model.activePageId];
|
|
318
|
+
const headerStyle = options.headerStyle?.({
|
|
319
|
+
model,
|
|
320
|
+
activePage,
|
|
321
|
+
pageModel: activePageModel,
|
|
322
|
+
});
|
|
218
323
|
const title = options.title ?? 'App';
|
|
219
|
-
|
|
324
|
+
let cursor = visibleLength(title) + 2;
|
|
325
|
+
const tabTargets = [];
|
|
326
|
+
const tabs = model.pageOrder.map((id, index) => {
|
|
220
327
|
const page = pagesById.get(id);
|
|
221
|
-
|
|
328
|
+
const label = id === model.activePageId ? `[${page.title}]` : ` ${page.title} `;
|
|
329
|
+
const width = visibleLength(label);
|
|
330
|
+
const startCol = cursor;
|
|
331
|
+
const endCol = cursor + width - 1;
|
|
332
|
+
if (endCol >= 0 && startCol < model.columns) {
|
|
333
|
+
tabTargets.push({
|
|
334
|
+
pageId: id,
|
|
335
|
+
startCol: Math.max(0, startCol),
|
|
336
|
+
endCol: Math.min(Math.max(0, model.columns - 1), endCol),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
cursor += width;
|
|
340
|
+
if (index < model.pageOrder.length - 1) {
|
|
341
|
+
cursor += 1;
|
|
342
|
+
}
|
|
343
|
+
return label;
|
|
222
344
|
}).join(' ');
|
|
223
345
|
const line = fitLine(`${title} ${tabs}`, model.columns);
|
|
224
|
-
|
|
346
|
+
const surface = createStyledTextSurfaceWithBCSS(line, model.columns, ctx, {
|
|
225
347
|
type: 'FrameHeader',
|
|
226
348
|
id: 'frame-header',
|
|
227
349
|
classes: [`page-${model.activePageId}`],
|
|
228
350
|
});
|
|
351
|
+
paintActiveHeaderTab(surface, tabTargets, model.activePageId, ctx, headerStyle?.activeTabToken);
|
|
352
|
+
return {
|
|
353
|
+
surface,
|
|
354
|
+
tabTargets,
|
|
355
|
+
};
|
|
229
356
|
}
|
|
230
|
-
/** Render the
|
|
231
|
-
export function renderHelpLine(model,
|
|
232
|
-
const mode =
|
|
357
|
+
/** Render the footer status line showing mode, focused pane, and key hints. */
|
|
358
|
+
export function renderHelpLine(model, activeLayer, i18n, notificationCue) {
|
|
359
|
+
const mode = activeLayer.kind === 'search' || activeLayer.kind === 'command-palette'
|
|
233
360
|
? 'PALETTE'
|
|
234
|
-
:
|
|
361
|
+
: activeLayer.kind === 'help'
|
|
235
362
|
? 'HELP'
|
|
236
|
-
: '
|
|
363
|
+
: activeLayer.kind === 'quit-confirm'
|
|
364
|
+
? 'QUIT'
|
|
365
|
+
: activeLayer.kind === 'settings'
|
|
366
|
+
? 'SETTINGS'
|
|
367
|
+
: activeLayer.kind === 'notification-center'
|
|
368
|
+
? 'NOTICES'
|
|
369
|
+
: activeLayer.kind === 'page-modal'
|
|
370
|
+
? 'MODAL'
|
|
371
|
+
: 'NORMAL';
|
|
237
372
|
const focusedPane = model.focusedPaneByPage[model.activePageId] ?? '-';
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
model
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
373
|
+
const modeLabel = frameModeLabel(i18n, mode);
|
|
374
|
+
const status = notificationCue == null || notificationCue.length === 0
|
|
375
|
+
? `[${modeLabel}] page:${model.activePageId} pane:${focusedPane}`
|
|
376
|
+
: `[${modeLabel}] page:${model.activePageId} pane:${focusedPane} ${notificationCue}`;
|
|
377
|
+
const hint = typeof activeLayer.hintSource === 'string'
|
|
378
|
+
? activeLayer.hintSource
|
|
379
|
+
: activeLayer.hintSource == null
|
|
380
|
+
? ''
|
|
381
|
+
: helpShort(activeLayer.hintSource);
|
|
246
382
|
const line = hint.length > 0
|
|
247
|
-
?
|
|
383
|
+
? (() => {
|
|
384
|
+
const statusWithPadding = ` ${status}`;
|
|
385
|
+
const gap = model.columns - visibleLength(statusWithPadding) - visibleLength(hint);
|
|
386
|
+
return gap >= 2
|
|
387
|
+
? `${statusWithPadding}${' '.repeat(gap)}${hint}`
|
|
388
|
+
: `${statusWithPadding} ${hint}`;
|
|
389
|
+
})()
|
|
248
390
|
: ` ${status}`;
|
|
249
|
-
return
|
|
391
|
+
return createStyledTextSurfaceWithBCSS(fitLine(line, model.columns), model.columns, resolveSafeCtx(), {
|
|
250
392
|
type: 'FrameHelp',
|
|
251
393
|
id: 'frame-help',
|
|
252
394
|
classes: [`mode-${mode.toLowerCase()}`, `page-${model.activePageId}`],
|
|
253
395
|
});
|
|
254
396
|
}
|
|
255
397
|
/**
|
|
256
|
-
* Split a styled multiline string into a 2D grid of single-column characters.
|
|
257
|
-
* Each cell is a fully-styled string (including resets).
|
|
258
|
-
*/
|
|
259
|
-
export function stringToGrid(str, width, height) {
|
|
260
|
-
const lines = str.split('\n');
|
|
261
|
-
const grid = [];
|
|
262
|
-
for (let y = 0; y < height; y++) {
|
|
263
|
-
const line = lines[y] ?? '';
|
|
264
|
-
grid.push(tokenizeAnsi(line, width));
|
|
265
|
-
}
|
|
266
|
-
return grid;
|
|
267
|
-
}
|
|
268
398
|
/**
|
|
269
399
|
* Apply a transition shader to blend between the previous and next page views.
|
|
270
400
|
*
|
|
@@ -273,52 +403,69 @@ export function stringToGrid(str, width, height) {
|
|
|
273
403
|
*/
|
|
274
404
|
export function renderTransition(prev, next, style, progress, width, height, ctx, frame = 0) {
|
|
275
405
|
const shader = typeof style === 'function' ? style : TRANSITION_SHADERS[style];
|
|
276
|
-
if (!shader)
|
|
406
|
+
if (!shader || width <= 0 || height <= 0)
|
|
277
407
|
return next;
|
|
278
|
-
|
|
279
|
-
return next;
|
|
280
|
-
const prevGrid = stringToGrid(prev, width, height);
|
|
281
|
-
const nextGrid = stringToGrid(next, width, height);
|
|
282
|
-
const lines = [];
|
|
408
|
+
const surface = createSurface(width, height);
|
|
283
409
|
for (let y = 0; y < height; y++) {
|
|
284
|
-
let line = '';
|
|
285
410
|
for (let x = 0; x < width; x++) {
|
|
286
|
-
// Shared stable-ish pseudo-random seed based on coordinates
|
|
287
411
|
const seed = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
|
|
288
412
|
const rand = seed - Math.floor(seed);
|
|
289
413
|
const result = shader({ x, y, width, height, progress, rand, frame, ctx });
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
if (charOverride !== undefined) {
|
|
293
|
-
line += charOverride;
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
line += (showNext ? nextGrid[y]?.[x] : prevGrid[y]?.[x]) ?? ' ';
|
|
297
|
-
}
|
|
414
|
+
const baseCell = result.showNext ? next.get(x, y) : prev.get(x, y);
|
|
415
|
+
surface.set(x, y, applyTransitionCell(baseCell, result));
|
|
298
416
|
}
|
|
299
|
-
lines.push(line);
|
|
300
417
|
}
|
|
301
|
-
return
|
|
418
|
+
return surface;
|
|
302
419
|
}
|
|
303
|
-
export function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
return surfaceToPlainText(surface);
|
|
420
|
+
export function framePaneOutputToSurface(output, width, height, scratch) {
|
|
421
|
+
return scratch == null
|
|
422
|
+
? normalizeViewOutput(output, { width, height }).surface
|
|
423
|
+
: normalizeViewOutputInto(output, { width, height }, scratch).surface;
|
|
424
|
+
}
|
|
425
|
+
export function blockSurface(content, width, height) {
|
|
426
|
+
return parseAnsiToSurface(fitBlock(content, width, height).join('\n'), width, height);
|
|
312
427
|
}
|
|
313
|
-
function
|
|
314
|
-
const
|
|
315
|
-
for (let y = 0; y <
|
|
316
|
-
let
|
|
317
|
-
|
|
318
|
-
line += surface.get(x, y).char;
|
|
428
|
+
function paintDivider(target, rect, dividerChar, direction) {
|
|
429
|
+
const unit = resolveDividerUnit(dividerChar, direction === 'row' ? '│' : '─');
|
|
430
|
+
for (let y = 0; y < rect.height; y++) {
|
|
431
|
+
for (let x = 0; x < rect.width; x++) {
|
|
432
|
+
target.set(rect.col + x, rect.row + y, { char: unit, empty: false });
|
|
319
433
|
}
|
|
320
|
-
lines.push(line);
|
|
321
434
|
}
|
|
322
|
-
|
|
435
|
+
}
|
|
436
|
+
function resolveDividerUnit(dividerChar, fallback) {
|
|
437
|
+
if (dividerChar == null || dividerChar.length === 0)
|
|
438
|
+
return fallback;
|
|
439
|
+
return dividerChar[0] ?? fallback;
|
|
440
|
+
}
|
|
441
|
+
function getFramePaneScratch(width, height) {
|
|
442
|
+
const key = `${width}x${height}`;
|
|
443
|
+
let scratch = framePaneScratchBySize.get(key);
|
|
444
|
+
if (scratch == null) {
|
|
445
|
+
scratch = createSurface(width, height);
|
|
446
|
+
framePaneScratchBySize.set(key, scratch);
|
|
447
|
+
}
|
|
448
|
+
return scratch;
|
|
449
|
+
}
|
|
450
|
+
function applyTransitionCell(baseCell, result) {
|
|
451
|
+
if (result.overrideCell != null) {
|
|
452
|
+
return {
|
|
453
|
+
...baseCell,
|
|
454
|
+
...result.overrideCell,
|
|
455
|
+
char: result.overrideCell.char,
|
|
456
|
+
empty: false,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
if (result.overrideChar !== undefined) {
|
|
460
|
+
return {
|
|
461
|
+
...baseCell,
|
|
462
|
+
char: result.overrideChar,
|
|
463
|
+
empty: false,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
...baseCell,
|
|
468
|
+
empty: false,
|
|
469
|
+
};
|
|
323
470
|
}
|
|
324
471
|
//# sourceMappingURL=app-frame-render.js.map
|