]>
Commit | Line | Data |
---|---|---|
ca4e99e8 DI |
1 | /** |
2 | * @license MIT | |
3 | */ | |
4 | ||
92068f36 | 5 | import { ITerminal } from './Interfaces'; |
23e6a475 | 6 | import { DomElementObjectPool } from './utils/DomElementObjectPool'; |
92068f36 DI |
7 | |
8 | /** | |
9 | * The maximum number of refresh frames to skip when the write buffer is non- | |
10 | * empty. Note that these frames may be intermingled with frames that are | |
11 | * skipped via requestAnimationFrame's mechanism. | |
12 | */ | |
13 | const MAX_REFRESH_FRAME_SKIP = 5; | |
14 | ||
92068f36 | 15 | /** |
20f22fb6 | 16 | * Flags used to render terminal text properly. |
92068f36 | 17 | */ |
20f22fb6 DI |
18 | enum FLAGS { |
19 | BOLD = 1, | |
20 | UNDERLINE = 2, | |
21 | BLINK = 4, | |
22 | INVERSE = 8, | |
23 | INVISIBLE = 16 | |
92068f36 DI |
24 | }; |
25 | ||
26 | let brokenBold: boolean = null; | |
27 | ||
28 | export class Renderer { | |
29 | /** A queue of the rows to be refreshed */ | |
30 | private _refreshRowsQueue: {start: number, end: number}[] = []; | |
31 | private _refreshFramesSkipped = 0; | |
32 | private _refreshAnimationFrame = null; | |
33 | ||
23e6a475 DI |
34 | private _spanElementObjectPool = new DomElementObjectPool('span'); |
35 | ||
92068f36 DI |
36 | constructor(private _terminal: ITerminal) { |
37 | // Figure out whether boldness affects | |
38 | // the character width of monospace fonts. | |
39 | if (brokenBold === null) { | |
f0d5b401 | 40 | brokenBold = checkBoldBroken((<any>this._terminal).element); |
92068f36 | 41 | } |
23e6a475 | 42 | this._spanElementObjectPool = new DomElementObjectPool('span'); |
92068f36 | 43 | |
9d8f6c9a DI |
44 | // TODO: Pull more DOM interactions into Renderer.constructor, element for |
45 | // example should be owned by Renderer (and also exposed by Terminal due to | |
46 | // to established public API). | |
92068f36 DI |
47 | } |
48 | ||
49 | /** | |
50 | * Queues a refresh between two rows (inclusive), to be done on next animation | |
51 | * frame. | |
52 | * @param {number} start The start row. | |
53 | * @param {number} end The end row. | |
54 | */ | |
55 | public queueRefresh(start: number, end: number): void { | |
56 | this._refreshRowsQueue.push({ start: start, end: end }); | |
57 | if (!this._refreshAnimationFrame) { | |
58 | this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); | |
59 | } | |
60 | } | |
61 | ||
62 | /** | |
63 | * Performs the refresh loop callback, calling refresh only if a refresh is | |
64 | * necessary before queueing up the next one. | |
65 | */ | |
66 | private _refreshLoop(): void { | |
67 | // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it | |
68 | // will need to be immediately refreshed anyway. This saves a lot of | |
69 | // rendering time as the viewport DOM does not need to be refreshed, no | |
70 | // scroll events, no layouts, etc. | |
71 | const skipFrame = this._terminal.writeBuffer.length > 0 && this._refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; | |
72 | if (skipFrame) { | |
73 | this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this)); | |
74 | return; | |
75 | } | |
76 | ||
77 | this._refreshFramesSkipped = 0; | |
78 | let start; | |
79 | let end; | |
80 | if (this._refreshRowsQueue.length > 4) { | |
81 | // Just do a full refresh when 5+ refreshes are queued | |
82 | start = 0; | |
83 | end = this._terminal.rows - 1; | |
84 | } else { | |
85 | // Get start and end rows that need refreshing | |
86 | start = this._refreshRowsQueue[0].start; | |
87 | end = this._refreshRowsQueue[0].end; | |
88 | for (let i = 1; i < this._refreshRowsQueue.length; i++) { | |
89 | if (this._refreshRowsQueue[i].start < start) { | |
90 | start = this._refreshRowsQueue[i].start; | |
91 | } | |
92 | if (this._refreshRowsQueue[i].end > end) { | |
93 | end = this._refreshRowsQueue[i].end; | |
94 | } | |
95 | } | |
96 | } | |
97 | this._refreshRowsQueue = []; | |
98 | this._refreshAnimationFrame = null; | |
99 | this._refresh(start, end); | |
100 | } | |
101 | ||
102 | /** | |
103 | * Refreshes (re-renders) terminal content within two rows (inclusive) | |
104 | * | |
105 | * Rendering Engine: | |
106 | * | |
107 | * In the screen buffer, each character is stored as a an array with a character | |
108 | * and a 32-bit integer: | |
109 | * - First value: a utf-16 character. | |
110 | * - Second value: | |
111 | * - Next 9 bits: background color (0-511). | |
112 | * - Next 9 bits: foreground color (0-511). | |
113 | * - Next 14 bits: a mask for misc. flags: | |
114 | * - 1=bold | |
115 | * - 2=underline | |
116 | * - 4=blink | |
117 | * - 8=inverse | |
118 | * - 16=invisible | |
119 | * | |
120 | * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) | |
121 | * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) | |
122 | */ | |
123 | private _refresh(start: number, end: number): void { | |
92068f36 | 124 | // If this is a big refresh, remove the terminal rows from the DOM for faster calculations |
9ec89745 | 125 | let parent; |
92068f36 DI |
126 | if (end - start >= this._terminal.rows / 2) { |
127 | parent = this._terminal.element.parentNode; | |
128 | if (parent) { | |
129 | this._terminal.element.removeChild(this._terminal.rowContainer); | |
130 | } | |
131 | } | |
132 | ||
c887c6e0 DI |
133 | let width = this._terminal.cols; |
134 | let y = start; | |
92068f36 DI |
135 | |
136 | if (end >= this._terminal.rows) { | |
137 | this._terminal.log('`end` is too large. Most likely a bad CSR.'); | |
138 | end = this._terminal.rows - 1; | |
139 | } | |
140 | ||
141 | for (; y <= end; y++) { | |
c887c6e0 | 142 | let row = y + this._terminal.ydisp; |
92068f36 | 143 | |
c887c6e0 | 144 | let line = this._terminal.lines.get(row); |
92068f36 | 145 | |
c887c6e0 | 146 | let x; |
9ec89745 DI |
147 | if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp) && |
148 | this._terminal.cursorState && | |
149 | !this._terminal.cursorHidden) { | |
92068f36 DI |
150 | x = this._terminal.x; |
151 | } else { | |
152 | x = -1; | |
153 | } | |
154 | ||
c887c6e0 | 155 | let attr = this._terminal.defAttr; |
92068f36 | 156 | |
47814847 DI |
157 | const documentFragment = document.createDocumentFragment(); |
158 | let innerHTML = ''; | |
159 | let currentElement; | |
23e6a475 DI |
160 | |
161 | // Return the row's spans to the pool | |
162 | while (this._terminal.children[y].children.length) { | |
47814847 | 163 | const child = this._terminal.children[y].children[0]; |
23e6a475 DI |
164 | this._terminal.children[y].removeChild(child); |
165 | this._spanElementObjectPool.release(<HTMLElement>child); | |
166 | } | |
167 | ||
c887c6e0 DI |
168 | for (let i = 0; i < width; i++) { |
169 | // TODO: Could data be a more specific type? | |
170 | let data: any = line[i][0]; | |
47814847 DI |
171 | const ch = line[i][1]; |
172 | const ch_width: any = line[i][2]; | |
9ec89745 | 173 | if (!ch_width) { |
92068f36 | 174 | continue; |
9ec89745 | 175 | } |
92068f36 | 176 | |
9ec89745 DI |
177 | if (i === x) { |
178 | data = -1; | |
179 | } | |
92068f36 DI |
180 | |
181 | if (data !== attr) { | |
182 | if (attr !== this._terminal.defAttr) { | |
23e6a475 DI |
183 | if (innerHTML) { |
184 | currentElement.innerHTML = innerHTML; | |
185 | innerHTML = ''; | |
186 | } | |
187 | documentFragment.appendChild(currentElement); | |
188 | currentElement = null; | |
92068f36 DI |
189 | } |
190 | if (data !== this._terminal.defAttr) { | |
23e6a475 DI |
191 | if (innerHTML && !currentElement) { |
192 | currentElement = this._spanElementObjectPool.acquire(); | |
23e6a475 DI |
193 | } |
194 | if (currentElement) { | |
195 | if (innerHTML) { | |
196 | currentElement.innerHTML = innerHTML; | |
197 | innerHTML = ''; | |
198 | } | |
199 | documentFragment.appendChild(currentElement); | |
200 | } | |
201 | currentElement = this._spanElementObjectPool.acquire(); | |
92068f36 | 202 | if (data === -1) { |
23e6a475 | 203 | currentElement.classList.add('reverse-video', 'terminal-cursor'); |
92068f36 | 204 | } else { |
c887c6e0 DI |
205 | let bg = data & 0x1ff; |
206 | let fg = (data >> 9) & 0x1ff; | |
207 | let flags = data >> 18; | |
92068f36 DI |
208 | |
209 | if (flags & FLAGS.BOLD) { | |
210 | if (!brokenBold) { | |
23e6a475 | 211 | currentElement.classList.add('xterm-bold'); |
92068f36 DI |
212 | } |
213 | // See: XTerm*boldColors | |
9ec89745 DI |
214 | if (fg < 8) { |
215 | fg += 8; | |
216 | } | |
92068f36 DI |
217 | } |
218 | ||
219 | if (flags & FLAGS.UNDERLINE) { | |
23e6a475 | 220 | currentElement.classList.add('xterm-underline'); |
92068f36 DI |
221 | } |
222 | ||
223 | if (flags & FLAGS.BLINK) { | |
23e6a475 | 224 | currentElement.classList.add('xterm-blink'); |
92068f36 DI |
225 | } |
226 | ||
227 | // If inverse flag is on, then swap the foreground and background variables. | |
228 | if (flags & FLAGS.INVERSE) { | |
9ec89745 DI |
229 | let temp = bg; |
230 | bg = fg; | |
231 | fg = temp; | |
232 | // Should inverse just be before the above boldColors effect instead? | |
233 | if ((flags & 1) && fg < 8) { | |
234 | fg += 8; | |
235 | } | |
92068f36 DI |
236 | } |
237 | ||
238 | if (flags & FLAGS.INVISIBLE) { | |
23e6a475 | 239 | currentElement.classList.add('xterm-hidden'); |
92068f36 DI |
240 | } |
241 | ||
242 | /** | |
243 | * Weird situation: Invert flag used black foreground and white background results | |
244 | * in invalid background color, positioned at the 256 index of the 256 terminal | |
245 | * color map. Pin the colors manually in such a case. | |
246 | * | |
247 | * Source: https://github.com/sourcelair/xterm.js/issues/57 | |
248 | */ | |
249 | if (flags & FLAGS.INVERSE) { | |
aad84395 | 250 | if (bg === 257) { |
92068f36 DI |
251 | bg = 15; |
252 | } | |
aad84395 | 253 | if (fg === 256) { |
92068f36 DI |
254 | fg = 0; |
255 | } | |
256 | } | |
257 | ||
258 | if (bg < 256) { | |
cf863d48 | 259 | currentElement.classList.add(`xterm-bg-color-${bg}`); |
92068f36 DI |
260 | } |
261 | ||
262 | if (fg < 256) { | |
cf863d48 | 263 | currentElement.classList.add(`xterm-color-${fg}`); |
92068f36 | 264 | } |
92068f36 DI |
265 | } |
266 | } | |
267 | } | |
268 | ||
269 | if (ch_width === 2) { | |
cf863d48 DI |
270 | // Wrap wide characters so they're sized correctly. It's more difficult to release these |
271 | // from the object pool so just create new ones via innerHTML. | |
6c8949bb DI |
272 | innerHTML += `<span class="xterm-wide-char">${ch}</span>`; |
273 | } else if (ch.charCodeAt(0) > 255) { | |
274 | // Wrap any non-wide unicode character as some fonts size them badly | |
275 | innerHTML += `<span class="xterm-normal-char">${ch}</span>`; | |
3baa6b92 | 276 | } else { |
6c8949bb DI |
277 | switch (ch) { |
278 | case '&': | |
279 | innerHTML += '&'; | |
280 | break; | |
281 | case '<': | |
282 | innerHTML += '<'; | |
283 | break; | |
284 | case '>': | |
285 | innerHTML += '>'; | |
286 | break; | |
287 | default: | |
288 | if (ch <= ' ') { | |
289 | innerHTML += ' '; | |
290 | } else { | |
291 | innerHTML += ch; | |
292 | } | |
293 | break; | |
294 | } | |
92068f36 DI |
295 | } |
296 | ||
297 | attr = data; | |
298 | } | |
299 | ||
23e6a475 DI |
300 | if (innerHTML && !currentElement) { |
301 | currentElement = this._spanElementObjectPool.acquire(); | |
92068f36 | 302 | } |
23e6a475 DI |
303 | if (currentElement) { |
304 | if (innerHTML) { | |
305 | currentElement.innerHTML = innerHTML; | |
306 | innerHTML = ''; | |
307 | } | |
308 | documentFragment.appendChild(currentElement); | |
309 | currentElement = null; | |
310 | } | |
92068f36 | 311 | |
aad84395 | 312 | this._terminal.children[y].appendChild(documentFragment); |
92068f36 DI |
313 | } |
314 | ||
315 | if (parent) { | |
316 | this._terminal.element.appendChild(this._terminal.rowContainer); | |
317 | } | |
318 | ||
319 | this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end}); | |
320 | }; | |
b594407c | 321 | |
ec3bf113 DI |
322 | /** |
323 | * Refreshes the selection in the DOM. | |
324 | * @param start The selection start. | |
325 | * @param end The selection end. | |
326 | */ | |
b594407c | 327 | public refreshSelection(start: [number, number], end: [number, number]) { |
ad3ae67e DI |
328 | // Remove all selections |
329 | while (this._terminal.selectionContainer.children.length) { | |
330 | this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); | |
331 | } | |
332 | ||
333 | // Selection does not exist | |
334 | if (!start || !end) { | |
335 | return; | |
336 | } | |
337 | ||
ad3ae67e DI |
338 | // Translate from buffer position to viewport position |
339 | const viewportStartRow = start[1] - this._terminal.ydisp; | |
340 | const viewportEndRow = end[1] - this._terminal.ydisp; | |
341 | const viewportCappedStartRow = Math.max(viewportStartRow, 0); | |
342 | const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); | |
343 | ||
344 | // No need to draw the selection | |
345 | if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { | |
346 | return; | |
347 | } | |
348 | ||
ad3ae67e DI |
349 | // Create the selections |
350 | const documentFragment = document.createDocumentFragment(); | |
351 | // Draw first row | |
352 | const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; | |
353 | const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; | |
354 | documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); | |
355 | // Draw middle rows | |
9246d524 DI |
356 | const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; |
357 | documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); | |
ad3ae67e DI |
358 | // Draw final row |
359 | if (viewportCappedStartRow !== viewportCappedEndRow) { | |
360 | // Only draw viewportEndRow if it's not the same as viewporttartRow | |
361 | const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; | |
362 | documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); | |
363 | } | |
364 | this._terminal.selectionContainer.appendChild(documentFragment); | |
365 | } | |
366 | ||
71477874 DI |
367 | /** |
368 | * Creates a selection element at the specified position. | |
369 | * @param row The row of the selection. | |
370 | * @param colStart The start column. | |
371 | * @param colEnd The end columns. | |
372 | */ | |
9246d524 | 373 | private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { |
ad3ae67e | 374 | const element = document.createElement('div'); |
9246d524 | 375 | element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; |
ad3ae67e DI |
376 | element.style.top = `${row * this._terminal.charMeasure.height}px`; |
377 | element.style.left = `${colStart * this._terminal.charMeasure.width}px`; | |
378 | element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; | |
379 | return element; | |
b594407c | 380 | } |
92068f36 DI |
381 | } |
382 | ||
383 | ||
cf863d48 | 384 | // If bold is broken, we can't use it in the terminal. |
f0d5b401 | 385 | function checkBoldBroken(terminal) { |
386 | const document = terminal.ownerDocument; | |
92068f36 DI |
387 | const el = document.createElement('span'); |
388 | el.innerHTML = 'hello world'; | |
f0d5b401 | 389 | terminal.appendChild(el); |
2ec756fd | 390 | const w1 = el.offsetWidth; |
4b2ae6a7 | 391 | const h1 = el.offsetHeight; |
92068f36 | 392 | el.style.fontWeight = 'bold'; |
2ec756fd | 393 | const w2 = el.offsetWidth; |
4b2ae6a7 | 394 | const h2 = el.offsetHeight; |
f0d5b401 | 395 | terminal.removeChild(el); |
4b2ae6a7 | 396 | return w1 !== w2 || h1 !== h2; |
92068f36 | 397 | } |