]>
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++) { | |
bbafdd3d | 142 | let row = y + this._terminal.buffer.ydisp; |
92068f36 | 143 | |
8ede1fc9 | 144 | let line = this._terminal.buffer.lines.get(row); |
92068f36 | 145 | |
c887c6e0 | 146 | let x; |
73b4bfb5 | 147 | if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) && |
9ec89745 DI |
148 | this._terminal.cursorState && |
149 | !this._terminal.cursorHidden) { | |
73b4bfb5 | 150 | x = this._terminal.buffer.x; |
92068f36 DI |
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]; | |
5bc0ad87 | 173 | const isCursor: boolean = i === x; |
9ec89745 | 174 | if (!ch_width) { |
92068f36 | 175 | continue; |
9ec89745 | 176 | } |
92068f36 | 177 | |
5bc0ad87 | 178 | if (data !== attr || isCursor) { |
179 | if (attr !== this._terminal.defAttr && !isCursor) { | |
23e6a475 DI |
180 | if (innerHTML) { |
181 | currentElement.innerHTML = innerHTML; | |
182 | innerHTML = ''; | |
183 | } | |
184 | documentFragment.appendChild(currentElement); | |
185 | currentElement = null; | |
92068f36 | 186 | } |
5bc0ad87 | 187 | if (data !== this._terminal.defAttr || isCursor) { |
23e6a475 DI |
188 | if (innerHTML && !currentElement) { |
189 | currentElement = this._spanElementObjectPool.acquire(); | |
23e6a475 DI |
190 | } |
191 | if (currentElement) { | |
192 | if (innerHTML) { | |
193 | currentElement.innerHTML = innerHTML; | |
194 | innerHTML = ''; | |
195 | } | |
196 | documentFragment.appendChild(currentElement); | |
197 | } | |
198 | currentElement = this._spanElementObjectPool.acquire(); | |
5bc0ad87 | 199 | |
200 | let bg = data & 0x1ff; | |
201 | let fg = (data >> 9) & 0x1ff; | |
202 | let flags = data >> 18; | |
203 | ||
204 | if (isCursor) { | |
7e210c7e LB |
205 | currentElement.classList.add('reverse-video'); |
206 | currentElement.classList.add('terminal-cursor'); | |
5bc0ad87 | 207 | } |
92068f36 | 208 | |
5bc0ad87 | 209 | if (flags & FLAGS.BOLD) { |
210 | if (!brokenBold) { | |
211 | currentElement.classList.add('xterm-bold'); | |
92068f36 | 212 | } |
5bc0ad87 | 213 | // See: XTerm*boldColors |
214 | if (fg < 8) { | |
215 | fg += 8; | |
92068f36 | 216 | } |
5bc0ad87 | 217 | } |
92068f36 | 218 | |
5bc0ad87 | 219 | if (flags & FLAGS.UNDERLINE) { |
220 | currentElement.classList.add('xterm-underline'); | |
221 | } | |
92068f36 | 222 | |
5bc0ad87 | 223 | if (flags & FLAGS.BLINK) { |
224 | currentElement.classList.add('xterm-blink'); | |
225 | } | |
92068f36 | 226 | |
5bc0ad87 | 227 | // If inverse flag is on, then swap the foreground and background variables. |
228 | if (flags & FLAGS.INVERSE) { | |
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; | |
92068f36 | 235 | } |
5bc0ad87 | 236 | } |
92068f36 | 237 | |
5bc0ad87 | 238 | if (flags & FLAGS.INVISIBLE && !isCursor) { |
239 | currentElement.classList.add('xterm-hidden'); | |
240 | } | |
92068f36 | 241 | |
5bc0ad87 | 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) { | |
250 | if (bg === 257) { | |
251 | bg = 15; | |
252 | } | |
253 | if (fg === 256) { | |
254 | fg = 0; | |
92068f36 | 255 | } |
92068f36 | 256 | } |
5bc0ad87 | 257 | |
258 | if (bg < 256) { | |
259 | currentElement.classList.add(`xterm-bg-color-${bg}`); | |
260 | } | |
261 | ||
262 | if (fg < 256) { | |
263 | currentElement.classList.add(`xterm-color-${fg}`); | |
264 | } | |
265 | ||
92068f36 DI |
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 | ||
5bc0ad87 | 297 | // The cursor needs its own element, therefore we set attr to -1 |
298 | // which will cause the next character to be rendered in a new element | |
299 | attr = isCursor ? -1 : data; | |
300 | ||
92068f36 DI |
301 | } |
302 | ||
23e6a475 DI |
303 | if (innerHTML && !currentElement) { |
304 | currentElement = this._spanElementObjectPool.acquire(); | |
92068f36 | 305 | } |
23e6a475 DI |
306 | if (currentElement) { |
307 | if (innerHTML) { | |
308 | currentElement.innerHTML = innerHTML; | |
309 | innerHTML = ''; | |
310 | } | |
311 | documentFragment.appendChild(currentElement); | |
312 | currentElement = null; | |
313 | } | |
92068f36 | 314 | |
aad84395 | 315 | this._terminal.children[y].appendChild(documentFragment); |
92068f36 DI |
316 | } |
317 | ||
318 | if (parent) { | |
319 | this._terminal.element.appendChild(this._terminal.rowContainer); | |
320 | } | |
321 | ||
322 | this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end}); | |
323 | }; | |
b594407c | 324 | |
ec3bf113 DI |
325 | /** |
326 | * Refreshes the selection in the DOM. | |
327 | * @param start The selection start. | |
328 | * @param end The selection end. | |
329 | */ | |
b594407c | 330 | public refreshSelection(start: [number, number], end: [number, number]) { |
ad3ae67e DI |
331 | // Remove all selections |
332 | while (this._terminal.selectionContainer.children.length) { | |
333 | this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]); | |
334 | } | |
335 | ||
336 | // Selection does not exist | |
337 | if (!start || !end) { | |
338 | return; | |
339 | } | |
340 | ||
ad3ae67e | 341 | // Translate from buffer position to viewport position |
bbafdd3d PK |
342 | const viewportStartRow = start[1] - this._terminal.buffer.ydisp; |
343 | const viewportEndRow = end[1] - this._terminal.buffer.ydisp; | |
ad3ae67e DI |
344 | const viewportCappedStartRow = Math.max(viewportStartRow, 0); |
345 | const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); | |
346 | ||
347 | // No need to draw the selection | |
348 | if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { | |
349 | return; | |
350 | } | |
351 | ||
ad3ae67e DI |
352 | // Create the selections |
353 | const documentFragment = document.createDocumentFragment(); | |
354 | // Draw first row | |
355 | const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; | |
356 | const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; | |
357 | documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); | |
358 | // Draw middle rows | |
9246d524 DI |
359 | const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; |
360 | documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); | |
ad3ae67e DI |
361 | // Draw final row |
362 | if (viewportCappedStartRow !== viewportCappedEndRow) { | |
363 | // Only draw viewportEndRow if it's not the same as viewporttartRow | |
364 | const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; | |
365 | documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); | |
366 | } | |
367 | this._terminal.selectionContainer.appendChild(documentFragment); | |
368 | } | |
369 | ||
71477874 DI |
370 | /** |
371 | * Creates a selection element at the specified position. | |
372 | * @param row The row of the selection. | |
373 | * @param colStart The start column. | |
374 | * @param colEnd The end columns. | |
375 | */ | |
9246d524 | 376 | private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { |
ad3ae67e | 377 | const element = document.createElement('div'); |
9246d524 | 378 | element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; |
ad3ae67e DI |
379 | element.style.top = `${row * this._terminal.charMeasure.height}px`; |
380 | element.style.left = `${colStart * this._terminal.charMeasure.width}px`; | |
381 | element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; | |
382 | return element; | |
b594407c | 383 | } |
92068f36 DI |
384 | } |
385 | ||
386 | ||
cf863d48 | 387 | // If bold is broken, we can't use it in the terminal. |
f0d5b401 | 388 | function checkBoldBroken(terminal) { |
389 | const document = terminal.ownerDocument; | |
92068f36 DI |
390 | const el = document.createElement('span'); |
391 | el.innerHTML = 'hello world'; | |
f0d5b401 | 392 | terminal.appendChild(el); |
2ec756fd | 393 | const w1 = el.offsetWidth; |
4b2ae6a7 | 394 | const h1 = el.offsetHeight; |
92068f36 | 395 | el.style.fontWeight = 'bold'; |
2ec756fd | 396 | const w2 = el.offsetWidth; |
4b2ae6a7 | 397 | const h2 = el.offsetHeight; |
f0d5b401 | 398 | terminal.removeChild(el); |
4b2ae6a7 | 399 | return w1 !== w2 || h1 !== h2; |
92068f36 | 400 | } |