xref: /haiku/src/apps/serialconnect/TermView.cpp (revision a5061ecec55353a5f394759473f1fd6df04890da)
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 ^ cell.attrs.reverse) {
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 0
139 				// FIXME B_NEGATIVE_FACE isn't actually implemented so we
140 				// instead swap the colors above
141 				if (cell.attrs.reverse)
142 					font.SetFace(B_NEGATIVE_FACE);
143 #endif
144 				if (cell.attrs.strike)
145 					font.SetFace(B_STRIKEOUT_FACE);
146 
147 				// TODO handle "font" (alternate fonts), dwl and dhl (double size)
148 
149 				SetFont(&font);
150 				DrawString(string);
151 				x += width * fFontWidth;
152 
153 				// Prepare for next cell
154 				cell = newCell;
155 				string = "";
156 				width = 0;
157 			}
158 
159 			if (pos.col == cursorPos.col && pos.row == cursorPos.row)
160 				isCursor = true;
161 			else
162 				isCursor = false;
163 
164 			if (newCell.chars[0] == 0) {
165 				string += " ";
166 				pos.col ++;
167 				width += 1;
168 			} else {
169 				char buffer[VTERM_MAX_CHARS_PER_CELL];
170 				wcstombs(buffer, (wchar_t*)newCell.chars,
171 					VTERM_MAX_CHARS_PER_CELL);
172 				string += buffer;
173 				width += newCell.width;
174 				pos.col += newCell.width;
175 			}
176 		}
177 	}
178 }
179 
180 
181 void
182 TermView::FrameResized(float width, float height)
183 {
184 	VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing,
185 				height - 2 * kBorderSpacing));
186 	vterm_set_size(fTerm, newSize.end_row, newSize.end_col);
187 }
188 
189 
190 void
191 TermView::GetPreferredSize(float* width, float* height)
192 {
193 	if (width != NULL)
194 		*width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1;
195 	if (height != NULL)
196 		*height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1;
197 }
198 
199 
200 void
201 TermView::KeyDown(const char* bytes, int32 numBytes)
202 {
203 	// Translate some keys to more usual VT100 escape codes
204 	switch (bytes[0]) {
205 		case B_UP_ARROW:
206 			numBytes = 3;
207 			bytes = "\x1B[A";
208 			break;
209 		case B_DOWN_ARROW:
210 			numBytes = 3;
211 			bytes = "\x1B[B";
212 			break;
213 		case B_RIGHT_ARROW:
214 			numBytes = 3;
215 			bytes = "\x1B[C";
216 			break;
217 		case B_LEFT_ARROW:
218 			numBytes = 3;
219 			bytes = "\x1B[D";
220 			break;
221 		case B_BACKSPACE:
222 			numBytes = 1;
223 			bytes = "\x7F";
224 			break;
225 		case '\n':
226 			numBytes = fLineTerminator.Length();
227 			bytes = fLineTerminator.String();
228 			break;
229 	}
230 
231 	// Send the bytes to the serial port
232 	BMessage* keyEvent = new BMessage(kMsgDataWrite);
233 	keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes);
234 	be_app_messenger.SendMessage(keyEvent);
235 }
236 
237 
238 void
239 TermView::MessageReceived(BMessage* message)
240 {
241 	switch (message->what)
242 	{
243 		case 'DATA':
244 		{
245 			entry_ref ref;
246 			if (message->FindRef("refs", &ref) == B_OK)
247 			{
248 				// The user just dropped a file on us
249 				// TODO send it by XMODEM or so
250 			}
251 			break;
252 		}
253 		default:
254 			BView::MessageReceived(message);
255 	}
256 }
257 
258 
259 void
260 TermView::SetLineTerminator(BString terminator)
261 {
262 	fLineTerminator = terminator;
263 }
264 
265 
266 void
267 TermView::PushBytes(const char* bytes, size_t length)
268 {
269 	vterm_push_bytes(fTerm, bytes, length);
270 }
271 
272 
273 void
274 TermView::Clear()
275 {
276 	while (fScrollBuffer.ItemAt(0)) {
277 		free(fScrollBuffer.RemoveItem((int32)0));
278 	}
279 
280 	vterm_state_reset(vterm_obtain_state(fTerm), 1);
281 	vterm_screen_reset(fTermScreen, 1);
282 
283 	_UpdateScrollbar();
284 }
285 
286 
287 // #pragma mark -
288 
289 
290 void
291 TermView::_Init()
292 {
293 	SetFont(be_fixed_font);
294 
295 	font_height height;
296 	GetFontHeight(&height);
297 	fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
298 		+ ceilf(height.leading));
299 	fFontWidth = (int)be_fixed_font->StringWidth("X");
300 	fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
301 
302 	fTermScreen = vterm_obtain_screen(fTerm);
303 	vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
304 	vterm_screen_reset(fTermScreen, 1);
305 
306 	vterm_parser_set_utf8(fTerm, 1);
307 
308 	VTermScreenCell cell;
309 	VTermPos firstPos;
310 	firstPos.row = 0;
311 	firstPos.col = 0;
312 	_GetCell(firstPos, cell);
313 
314 	rgb_color background;
315 	background.red = cell.bg.red;
316 	background.green = cell.bg.green;
317 	background.blue = cell.bg.blue;
318 	background.alpha = 255;
319 
320 	SetViewColor(background);
321 	SetLineTerminator("\n");
322 }
323 
324 
325 VTermRect
326 TermView::_PixelsToGlyphs(BRect pixels) const
327 {
328 	pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
329 
330 	VTermRect rect;
331 	rect.start_col = (int)floor(pixels.left / fFontWidth);
332 	rect.end_col = (int)ceil(pixels.right / fFontWidth);
333 	rect.start_row = (int)floor(pixels.top / fFontHeight);
334 	rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
335 #if 0
336 	printf(
337 		"TOP %d ch < %f px\n"
338 		"BTM %d ch < %f px\n"
339 		"LFT %d ch < %f px\n"
340 		"RGH %d ch < %f px\n",
341 		rect.start_row, pixels.top,
342 		rect.end_row, pixels.bottom,
343 		rect.start_col, pixels.left,
344 		rect.end_col, pixels.right
345 	);
346 #endif
347 	return rect;
348 }
349 
350 
351 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
352 {
353 	BRect rect;
354 	rect.top = glyphs.start_row * fFontHeight;
355 	rect.bottom = glyphs.end_row * fFontHeight;
356 	rect.left = glyphs.start_col * fFontWidth;
357 	rect.right = glyphs.end_col * fFontWidth;
358 
359 	rect.OffsetBy(kBorderSpacing, kBorderSpacing);
360 #if 0
361 	printf(
362 		"TOP %d ch > %f px (%f)\n"
363 		"BTM %d ch > %f px\n"
364 		"LFT %d ch > %f px (%f)\n"
365 		"RGH %d ch > %f px\n",
366 		glyphs.start_row, rect.top, fFontHeight,
367 		glyphs.end_row, rect.bottom,
368 		glyphs.start_col, rect.left, fFontWidth,
369 		glyphs.end_col, rect.right
370 	);
371 #endif
372 	return rect;
373 }
374 
375 
376 BRect
377 TermView::_GlyphsToPixels(const int width, const int height) const
378 {
379 	VTermRect rect;
380 	rect.start_row = 0;
381 	rect.start_col = 0;
382 	rect.end_row = height;
383 	rect.end_col = width;
384 	return _GlyphsToPixels(rect);
385 }
386 
387 
388 void
389 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
390 {
391 	// First handle cells from the normal screen
392 	if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
393 		return;
394 
395 	// Try the scroll-back buffer
396 	if (pos.row < 0 && pos.col >= 0) {
397 		int offset = - pos.row - 1;
398 		ScrollBufferItem* line
399 			= (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
400 		if (line != NULL && pos.col < line->cols) {
401 			cell = line->cells[pos.col];
402 			return;
403 		}
404 	}
405 
406 	// All cells outside the used terminal area are drawn with the same
407 	// background color as the top-left one.
408 	// TODO should they use the attributes of the closest neighbor instead?
409 	VTermPos firstPos;
410 	firstPos.row = 0;
411 	firstPos.col = 0;
412 	vterm_screen_get_cell(fTermScreen, firstPos, &cell);
413 	cell.chars[0] = 0;
414 	cell.width = 1;
415 }
416 
417 
418 void
419 TermView::_Damage(VTermRect rect)
420 {
421 	Invalidate(_GlyphsToPixels(rect));
422 }
423 
424 
425 void
426 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
427 {
428 	VTermRect r;
429 
430 	// We need to erase the cursor from its old position
431 	r.start_row = oldPos.row;
432 	r.start_col = oldPos.col;
433 	r.end_col = oldPos.col + 1;
434 	r.end_row = oldPos.row + 1;
435 	Invalidate(_GlyphsToPixels(r));
436 
437 	// And we need to draw it at the new one
438 	r.start_row = pos.row;
439 	r.start_col = pos.col;
440 	r.end_col = pos.col + 1;
441 	r.end_row = pos.row + 1;
442 	Invalidate(_GlyphsToPixels(r));
443 }
444 
445 
446 void
447 TermView::_PushLine(int cols, const VTermScreenCell* cells)
448 {
449 	ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
450 		+ cols * sizeof(VTermScreenCell));
451 	item->cols = cols;
452 	memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
453 
454 	fScrollBuffer.AddItem(item, 0);
455 
456 	// Remove extra items if the scrollback gets too long
457 	free(fScrollBuffer.RemoveItem(kScrollBackSize));
458 
459 	_UpdateScrollbar();
460 }
461 
462 
463 void
464 TermView::_UpdateScrollbar()
465 {
466 	int availableRows, availableCols;
467 	vterm_get_size(fTerm, &availableRows, &availableCols);
468 
469 	VTermRect dirty;
470 	dirty.start_col = 0;
471 	dirty.end_col = availableCols;
472 	dirty.end_row = 0;
473 	dirty.start_row = -fScrollBuffer.CountItems();
474 	// FIXME we should rather use CopyRect if possible, and only invalidate the
475 	// newly exposed area here.
476 	Invalidate(_GlyphsToPixels(dirty));
477 
478 	BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
479 	if (scrollBar != NULL) {
480 		float range = (fScrollBuffer.CountItems() + availableRows)
481 			* fFontHeight;
482 		scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
483 		// TODO we need to adjust this in FrameResized, as availableRows can
484 		// change
485 		scrollBar->SetProportion(availableRows * fFontHeight / range);
486 		scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
487 	}
488 }
489 
490 
491 int
492 TermView::_PopLine(int cols, VTermScreenCell* cells)
493 {
494 	ScrollBufferItem* item =
495 		(ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0);
496 	if (item == NULL)
497 		return 0;
498 
499 	_UpdateScrollbar();
500 	if (item->cols >= cols) {
501 		memcpy(cells, item->cells, cols * sizeof(VTermScreenCell));
502 	} else {
503 		memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell));
504 		for (int i = item->cols; i < cols; i++)
505 			cells[i] = cells[i - 1];
506 	}
507 	free(item);
508 	return 1;
509 }
510 
511 
512 /* static */ int
513 TermView::_Damage(VTermRect rect, void* user)
514 {
515 	TermView* view = (TermView*)user;
516 	view->_Damage(rect);
517 
518 	return 0;
519 }
520 
521 
522 /* static */ int
523 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
524 {
525 	TermView* view = (TermView*)user;
526 	view->_MoveCursor(pos, oldPos, visible);
527 
528 	return 0;
529 }
530 
531 
532 /* static */ int
533 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
534 {
535 	TermView* view = (TermView*)user;
536 	view->_PushLine(cols, cells);
537 
538 	return 0;
539 }
540 
541 
542 /* static */ int
543 TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
544 {
545 	TermView* view = (TermView*)user;
546 	return view->_PopLine(cols, cells);
547 }
548 
549 
550 const
551 VTermScreenCallbacks TermView::sScreenCallbacks = {
552 	&TermView::_Damage,
553 	/*.moverect =*/ NULL,
554 	&TermView::_MoveCursor,
555 	/*.settermprop =*/ NULL,
556 	/*.setmousefunc =*/ NULL,
557 	/*.bell =*/ NULL,
558 	/*.resize =*/ NULL,
559 	&TermView::_PushLine,
560 	&TermView::_PopLine,
561 };
562