xref: /haiku/src/apps/serialconnect/TermView.cpp (revision 759ee24c4c1722813609c3b7c77fabef275b02bf)
1 /*
2  * Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
3  * Distributed under the terms of the MIT licence.
4  */
5 
6 
7 #include "TermView.h"
8 
9 #include <stdio.h>
10 
11 #include <Entry.h>
12 #include <File.h>
13 #include <Font.h>
14 #include <Layout.h>
15 #include <ScrollBar.h>
16 
17 #include "SerialApp.h"
18 #include "libvterm/src/vterm_internal.h"
19 
20 
21 struct ScrollBufferItem {
22 	int cols;
23 	VTermScreenCell cells[];
24 };
25 
26 
27 TermView::TermView()
28 	:
29 	BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS)
30 {
31 	_Init();
32 }
33 
34 
35 TermView::TermView(BRect r)
36 	:
37 	BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS)
38 {
39 	_Init();
40 }
41 
42 
43 TermView::~TermView()
44 {
45 	vterm_free(fTerm);
46 }
47 
48 
49 void
50 TermView::AttachedToWindow()
51 {
52 	BView::AttachedToWindow();
53 	MakeFocus();
54 }
55 
56 
57 void
58 TermView::Draw(BRect updateRect)
59 {
60 	VTermRect updatedChars = _PixelsToGlyphs(updateRect);
61 
62 	VTermPos pos;
63 	font_height height;
64 	GetFontHeight(&height);
65 
66 	VTermPos cursorPos;
67 	vterm_state_get_cursorpos(vterm_obtain_state(fTerm), &cursorPos);
68 
69 	for (pos.row = updatedChars.start_row; pos.row <= updatedChars.end_row;
70 			pos.row++) {
71 		int x = updatedChars.start_col * fFontWidth + kBorderSpacing;
72 		int y = pos.row * fFontHeight + (int)ceil(height.ascent)
73 			+ kBorderSpacing;
74 		MovePenTo(x, y);
75 
76 		BString string;
77 		VTermScreenCell cell;
78 		int width = 0;
79 		bool isCursor = false;
80 
81 		pos.col = updatedChars.start_col;
82 		_GetCell(pos, cell);
83 
84 		for (pos.col = updatedChars.start_col;
85 				pos.col <= updatedChars.end_col;) {
86 
87 			VTermScreenCell newCell;
88 			_GetCell(pos, newCell);
89 
90 			// We need to start a new extent if:
91 			// - The attributes change
92 			// - The colors change
93 			// - The end of line is reached
94 			// - The current cell is under the cursor
95 			// - The current cell is right of the cursor
96 			if (*(uint32_t*)&cell.attrs != *(uint32_t*)&newCell.attrs
97 				|| !vterm_color_equal(cell.fg, newCell.fg)
98 				|| !vterm_color_equal(cell.bg, newCell.bg)
99 				|| pos.col >= updatedChars.end_col
100 				|| (pos.col == cursorPos.col && pos.row == cursorPos.row)
101 				|| (pos.col == cursorPos.col + 1 && pos.row == cursorPos.row)) {
102 
103 				rgb_color foreground, background;
104 				foreground.red = cell.fg.red;
105 				foreground.green = cell.fg.green;
106 				foreground.blue = cell.fg.blue;
107 				foreground.alpha = 255;
108 				background.red = cell.bg.red;
109 				background.green = cell.bg.green;
110 				background.blue = cell.bg.blue;
111 				background.alpha = 255;
112 
113 				// Draw the cursor by swapping foreground and background colors
114 				if (isCursor) {
115 					SetLowColor(foreground);
116 					SetViewColor(foreground);
117 					SetHighColor(background);
118 				} else {
119 					SetLowColor(background);
120 					SetViewColor(background);
121 					SetHighColor(foreground);
122 				}
123 
124 				FillRect(BRect(x, y - ceil(height.ascent) + 1,
125 					x + width * fFontWidth - 1,
126 					y + ceil(height.descent) + ceil(height.leading)),
127 					B_SOLID_LOW);
128 
129 				BFont font = be_fixed_font;
130 				if (cell.attrs.bold)
131 					font.SetFace(B_BOLD_FACE);
132 				if (cell.attrs.underline)
133 					font.SetFace(B_UNDERSCORE_FACE);
134 				if (cell.attrs.italic)
135 					font.SetFace(B_ITALIC_FACE);
136 				if (cell.attrs.blink) // FIXME make it actually blink
137 					font.SetFace(B_OUTLINED_FACE);
138 				if (cell.attrs.reverse)
139 					font.SetFace(B_NEGATIVE_FACE);
140 				if (cell.attrs.strike)
141 					font.SetFace(B_STRIKEOUT_FACE);
142 
143 				// TODO handle "font" (alternate fonts), dwl and dhl (double size)
144 
145 				SetFont(&font);
146 				DrawString(string);
147 				x += width * fFontWidth;
148 
149 				// Prepare for next cell
150 				cell = newCell;
151 				string = "";
152 				width = 0;
153 			}
154 
155 			if (pos.col == cursorPos.col && pos.row == cursorPos.row)
156 				isCursor = true;
157 			else
158 				isCursor = false;
159 
160 			if (newCell.chars[0] == 0) {
161 				string += " ";
162 				pos.col ++;
163 				width += 1;
164 			} else {
165 				char buffer[VTERM_MAX_CHARS_PER_CELL];
166 				wcstombs(buffer, (wchar_t*)newCell.chars,
167 					VTERM_MAX_CHARS_PER_CELL);
168 				string += buffer;
169 				width += newCell.width;
170 				pos.col += newCell.width;
171 			}
172 		}
173 	}
174 }
175 
176 
177 void
178 TermView::FrameResized(float width, float height)
179 {
180 	VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing,
181 				height - 2 * kBorderSpacing));
182 	vterm_set_size(fTerm, newSize.end_row, newSize.end_col);
183 }
184 
185 
186 void
187 TermView::GetPreferredSize(float* width, float* height)
188 {
189 	if (width != NULL)
190 		*width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1;
191 	if (height != NULL)
192 		*height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1;
193 }
194 
195 
196 void
197 TermView::KeyDown(const char* bytes, int32 numBytes)
198 {
199 	// Translate some keys to more usual VT100 escape codes
200 	switch (bytes[0]) {
201 		case B_UP_ARROW:
202 			numBytes = 3;
203 			bytes = "\x1B[A";
204 			break;
205 		case B_DOWN_ARROW:
206 			numBytes = 3;
207 			bytes = "\x1B[B";
208 			break;
209 		case B_RIGHT_ARROW:
210 			numBytes = 3;
211 			bytes = "\x1B[C";
212 			break;
213 		case B_LEFT_ARROW:
214 			numBytes = 3;
215 			bytes = "\x1B[D";
216 			break;
217 		case B_BACKSPACE:
218 			numBytes = 1;
219 			bytes = "\x7F";
220 			break;
221 		case '\n':
222 			numBytes = fLineTerminator.Length();
223 			bytes = fLineTerminator.String();
224 			break;
225 	}
226 
227 	// Send the bytes to the serial port
228 	BMessage* keyEvent = new BMessage(kMsgDataWrite);
229 	keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes);
230 	be_app_messenger.SendMessage(keyEvent);
231 }
232 
233 
234 void
235 TermView::MessageReceived(BMessage* message)
236 {
237 	switch (message->what)
238 	{
239 		case 'DATA':
240 		{
241 			entry_ref ref;
242 			if (message->FindRef("refs", &ref) == B_OK)
243 			{
244 				// The user just dropped a file on us
245 				// TODO send it by XMODEM or so
246 			}
247 			break;
248 		}
249 		default:
250 			BView::MessageReceived(message);
251 	}
252 }
253 
254 
255 void
256 TermView::SetLineTerminator(BString terminator)
257 {
258 	fLineTerminator = terminator;
259 }
260 
261 
262 void
263 TermView::PushBytes(const char* bytes, size_t length)
264 {
265 	vterm_push_bytes(fTerm, bytes, length);
266 }
267 
268 
269 void
270 TermView::Clear()
271 {
272 	while (fScrollBuffer.ItemAt(0)) {
273 		free(fScrollBuffer.RemoveItem((int32)0));
274 	}
275 
276 	vterm_state_reset(vterm_obtain_state(fTerm), 1);
277 	vterm_screen_reset(fTermScreen, 1);
278 
279 	_UpdateScrollbar();
280 }
281 
282 
283 // #pragma mark -
284 
285 
286 void
287 TermView::_Init()
288 {
289 	SetFont(be_fixed_font);
290 
291 	font_height height;
292 	GetFontHeight(&height);
293 	fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
294 		+ ceilf(height.leading));
295 	fFontWidth = (int)be_fixed_font->StringWidth("X");
296 	fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
297 
298 	fTermScreen = vterm_obtain_screen(fTerm);
299 	vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
300 	vterm_screen_reset(fTermScreen, 1);
301 
302 	vterm_parser_set_utf8(fTerm, 1);
303 
304 	VTermScreenCell cell;
305 	VTermPos firstPos;
306 	firstPos.row = 0;
307 	firstPos.col = 0;
308 	_GetCell(firstPos, cell);
309 
310 	rgb_color background;
311 	background.red = cell.bg.red;
312 	background.green = cell.bg.green;
313 	background.blue = cell.bg.blue;
314 	background.alpha = 255;
315 
316 	SetViewColor(background);
317 	SetLineTerminator("\n");
318 }
319 
320 
321 VTermRect
322 TermView::_PixelsToGlyphs(BRect pixels) const
323 {
324 	pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
325 
326 	VTermRect rect;
327 	rect.start_col = (int)floor(pixels.left / fFontWidth);
328 	rect.end_col = (int)ceil(pixels.right / fFontWidth);
329 	rect.start_row = (int)floor(pixels.top / fFontHeight);
330 	rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
331 #if 0
332 	printf(
333 		"TOP %d ch < %f px\n"
334 		"BTM %d ch < %f px\n"
335 		"LFT %d ch < %f px\n"
336 		"RGH %d ch < %f px\n",
337 		rect.start_row, pixels.top,
338 		rect.end_row, pixels.bottom,
339 		rect.start_col, pixels.left,
340 		rect.end_col, pixels.right
341 	);
342 #endif
343 	return rect;
344 }
345 
346 
347 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
348 {
349 	BRect rect;
350 	rect.top = glyphs.start_row * fFontHeight;
351 	rect.bottom = glyphs.end_row * fFontHeight;
352 	rect.left = glyphs.start_col * fFontWidth;
353 	rect.right = glyphs.end_col * fFontWidth;
354 
355 	rect.OffsetBy(kBorderSpacing, kBorderSpacing);
356 #if 0
357 	printf(
358 		"TOP %d ch > %f px (%f)\n"
359 		"BTM %d ch > %f px\n"
360 		"LFT %d ch > %f px (%f)\n"
361 		"RGH %d ch > %f px\n",
362 		glyphs.start_row, rect.top, fFontHeight,
363 		glyphs.end_row, rect.bottom,
364 		glyphs.start_col, rect.left, fFontWidth,
365 		glyphs.end_col, rect.right
366 	);
367 #endif
368 	return rect;
369 }
370 
371 
372 BRect
373 TermView::_GlyphsToPixels(const int width, const int height) const
374 {
375 	VTermRect rect;
376 	rect.start_row = 0;
377 	rect.start_col = 0;
378 	rect.end_row = height;
379 	rect.end_col = width;
380 	return _GlyphsToPixels(rect);
381 }
382 
383 
384 void
385 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
386 {
387 	// First handle cells from the normal screen
388 	if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
389 		return;
390 
391 	// Try the scroll-back buffer
392 	if (pos.row < 0 && pos.col >= 0) {
393 		int offset = - pos.row - 1;
394 		ScrollBufferItem* line
395 			= (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
396 		if (line != NULL && pos.col < line->cols) {
397 			cell = line->cells[pos.col];
398 			return;
399 		}
400 	}
401 
402 	// All cells outside the used terminal area are drawn with the same
403 	// background color as the top-left one.
404 	// TODO should they use the attributes of the closest neighbor instead?
405 	VTermPos firstPos;
406 	firstPos.row = 0;
407 	firstPos.col = 0;
408 	vterm_screen_get_cell(fTermScreen, firstPos, &cell);
409 	cell.chars[0] = 0;
410 	cell.width = 1;
411 }
412 
413 
414 void
415 TermView::_Damage(VTermRect rect)
416 {
417 	Invalidate(_GlyphsToPixels(rect));
418 }
419 
420 
421 void
422 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
423 {
424 	VTermRect r;
425 
426 	// We need to erase the cursor from its old position
427 	r.start_row = oldPos.row;
428 	r.start_col = oldPos.col;
429 	r.end_col = oldPos.col + 1;
430 	r.end_row = oldPos.row + 1;
431 	Invalidate(_GlyphsToPixels(r));
432 
433 	// And we need to draw it at the new one
434 	r.start_row = pos.row;
435 	r.start_col = pos.col;
436 	r.end_col = pos.col + 1;
437 	r.end_row = pos.row + 1;
438 	Invalidate(_GlyphsToPixels(r));
439 }
440 
441 
442 void
443 TermView::_PushLine(int cols, const VTermScreenCell* cells)
444 {
445 	ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
446 		+ cols * sizeof(VTermScreenCell));
447 	item->cols = cols;
448 	memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
449 
450 	fScrollBuffer.AddItem(item, 0);
451 
452 	// Remove extra items if the scrollback gets too long
453 	free(fScrollBuffer.RemoveItem(kScrollBackSize));
454 
455 	_UpdateScrollbar();
456 }
457 
458 
459 void
460 TermView::_UpdateScrollbar()
461 {
462 	int availableRows, availableCols;
463 	vterm_get_size(fTerm, &availableRows, &availableCols);
464 
465 	VTermRect dirty;
466 	dirty.start_col = 0;
467 	dirty.end_col = availableCols;
468 	dirty.end_row = 0;
469 	dirty.start_row = -fScrollBuffer.CountItems();
470 	// FIXME we should rather use CopyRect if possible, and only invalidate the
471 	// newly exposed area here.
472 	Invalidate(_GlyphsToPixels(dirty));
473 
474 	BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
475 	if (scrollBar != NULL) {
476 		float range = (fScrollBuffer.CountItems() + availableRows)
477 			* fFontHeight;
478 		scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
479 		// TODO we need to adjust this in FrameResized, as availableRows can
480 		// change
481 		scrollBar->SetProportion(availableRows * fFontHeight / range);
482 		scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
483 	}
484 }
485 
486 
487 int
488 TermView::_PopLine(int cols, VTermScreenCell* cells)
489 {
490 	ScrollBufferItem* item =
491 		(ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0);
492 	if (item == NULL)
493 		return 0;
494 
495 	_UpdateScrollbar();
496 	if (item->cols >= cols) {
497 		memcpy(cells, item->cells, cols * sizeof(VTermScreenCell));
498 	} else {
499 		memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell));
500 		for (int i = item->cols; i < cols; i++)
501 			cells[i] = cells[i - 1];
502 	}
503 	free(item);
504 	return 1;
505 }
506 
507 
508 /* static */ int
509 TermView::_Damage(VTermRect rect, void* user)
510 {
511 	TermView* view = (TermView*)user;
512 	view->_Damage(rect);
513 
514 	return 0;
515 }
516 
517 
518 /* static */ int
519 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
520 {
521 	TermView* view = (TermView*)user;
522 	view->_MoveCursor(pos, oldPos, visible);
523 
524 	return 0;
525 }
526 
527 
528 /* static */ int
529 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
530 {
531 	TermView* view = (TermView*)user;
532 	view->_PushLine(cols, cells);
533 
534 	return 0;
535 }
536 
537 
538 /* static */ int
539 TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
540 {
541 	TermView* view = (TermView*)user;
542 	return view->_PopLine(cols, cells);
543 }
544 
545 
546 const
547 VTermScreenCallbacks TermView::sScreenCallbacks = {
548 	&TermView::_Damage,
549 	/*.moverect =*/ NULL,
550 	&TermView::_MoveCursor,
551 	/*.settermprop =*/ NULL,
552 	/*.setmousefunc =*/ NULL,
553 	/*.bell =*/ NULL,
554 	/*.resize =*/ NULL,
555 	&TermView::_PushLine,
556 	&TermView::_PopLine,
557 };
558