Composite all overlays into content lines (sorted by focusOrder, higher = on top).
(lines: string[], termWidth: number, termHeight: number)
| 1040 | |
| 1041 | /** Composite all overlays into content lines (sorted by focusOrder, higher = on top). */ |
| 1042 | private compositeOverlays(lines: string[], termWidth: number, termHeight: number): string[] { |
| 1043 | if (this.overlayStack.length === 0) return lines; |
| 1044 | const result = [...lines]; |
| 1045 | |
| 1046 | // Pre-render all visible overlays and calculate positions |
| 1047 | const rendered: { overlayLines: string[]; row: number; col: number; w: number }[] = []; |
| 1048 | let minLinesNeeded = result.length; |
| 1049 | |
| 1050 | const visibleEntries = this.overlayStack.filter((e) => this.isOverlayVisible(e)); |
| 1051 | visibleEntries.sort((a, b) => a.focusOrder - b.focusOrder); |
| 1052 | for (const entry of visibleEntries) { |
| 1053 | const { component, options } = entry; |
| 1054 | |
| 1055 | // Get layout with height=0 first to determine width and maxHeight |
| 1056 | // (width and maxHeight don't depend on overlay height) |
| 1057 | const { width, maxHeight } = this.resolveOverlayLayout(options, 0, termWidth, termHeight); |
| 1058 | |
| 1059 | // Render component at calculated width |
| 1060 | let overlayLines = component.render(width); |
| 1061 | |
| 1062 | // Apply maxHeight if specified |
| 1063 | if (maxHeight !== undefined && overlayLines.length > maxHeight) { |
| 1064 | overlayLines = overlayLines.slice(0, maxHeight); |
| 1065 | } |
| 1066 | |
| 1067 | // Get final row/col with actual overlay height |
| 1068 | const { row, col } = this.resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight); |
| 1069 | |
| 1070 | rendered.push({ overlayLines, row, col, w: width }); |
| 1071 | minLinesNeeded = Math.max(minLinesNeeded, row + overlayLines.length); |
| 1072 | } |
| 1073 | |
| 1074 | // Pad to at least terminal height so overlays have screen-relative positions. |
| 1075 | // Excludes maxLinesRendered: the historical high-water mark caused self-reinforcing |
| 1076 | // inflation that pushed content into scrollback on terminal widen. |
| 1077 | const workingHeight = Math.max(result.length, termHeight, minLinesNeeded); |
| 1078 | |
| 1079 | // Extend result with empty lines if content is too short for overlay placement or working area |
| 1080 | while (result.length < workingHeight) { |
| 1081 | result.push(""); |
| 1082 | } |
| 1083 | |
| 1084 | const viewportStart = Math.max(0, workingHeight - termHeight); |
| 1085 | |
| 1086 | // Composite each overlay |
| 1087 | for (const { overlayLines, row, col, w } of rendered) { |
| 1088 | for (let i = 0; i < overlayLines.length; i++) { |
| 1089 | const idx = viewportStart + row + i; |
| 1090 | if (idx >= 0 && idx < result.length) { |
| 1091 | // Defensive: truncate overlay line to declared width before compositing |
| 1092 | // (components should already respect width, but this ensures it) |
| 1093 | const truncatedOverlayLine = |
| 1094 | visibleWidth(overlayLines[i]!) > w ? sliceByColumn(overlayLines[i]!, 0, w, true) : overlayLines[i]!; |
| 1095 | result[idx] = this.compositeLineAt(result[idx]!, truncatedOverlayLine, col, w, termWidth); |
| 1096 | } |
| 1097 | } |
| 1098 | } |
| 1099 |
no test coverage detected