xref: /haiku/src/apps/serialconnect/TermView.cpp (revision 9642f7705b27e5c270c15fa526d14e1848c2c27d)
1 /*
2  * Copyright 2012-2015, Adrien Destugues, pulkomandy@gmail.com
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 // #pragma mark -
270 
271 
272 void
273 TermView::_Init()
274 {
275 	SetFont(be_fixed_font);
276 
277 	font_height height;
278 	GetFontHeight(&height);
279 	fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
280 		+ ceilf(height.leading));
281 	fFontWidth = (int)be_fixed_font->StringWidth("X");
282 	fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
283 
284 	fTermScreen = vterm_obtain_screen(fTerm);
285 	vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
286 	vterm_screen_reset(fTermScreen, 1);
287 
288 	vterm_parser_set_utf8(fTerm, 1);
289 
290 	VTermScreenCell cell;
291 	VTermPos firstPos;
292 	firstPos.row = 0;
293 	firstPos.col = 0;
294 	_GetCell(firstPos, cell);
295 
296 	rgb_color background;
297 	background.red = cell.bg.red;
298 	background.green = cell.bg.green;
299 	background.blue = cell.bg.blue;
300 	background.alpha = 255;
301 
302 	SetViewColor(background);
303 	SetLineTerminator("\n");
304 }
305 
306 
307 VTermRect
308 TermView::_PixelsToGlyphs(BRect pixels) const
309 {
310 	pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
311 
312 	VTermRect rect;
313 	rect.start_col = (int)floor(pixels.left / fFontWidth);
314 	rect.end_col = (int)ceil(pixels.right / fFontWidth);
315 	rect.start_row = (int)floor(pixels.top / fFontHeight);
316 	rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
317 #if 0
318 	printf(
319 		"TOP %d ch < %f px\n"
320 		"BTM %d ch < %f px\n"
321 		"LFT %d ch < %f px\n"
322 		"RGH %d ch < %f px\n",
323 		rect.start_row, pixels.top,
324 		rect.end_row, pixels.bottom,
325 		rect.start_col, pixels.left,
326 		rect.end_col, pixels.right
327 	);
328 #endif
329 	return rect;
330 }
331 
332 
333 BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
334 {
335 	BRect rect;
336 	rect.top = glyphs.start_row * fFontHeight;
337 	rect.bottom = glyphs.end_row * fFontHeight;
338 	rect.left = glyphs.start_col * fFontWidth;
339 	rect.right = glyphs.end_col * fFontWidth;
340 
341 	rect.OffsetBy(kBorderSpacing, kBorderSpacing);
342 #if 0
343 	printf(
344 		"TOP %d ch > %f px (%f)\n"
345 		"BTM %d ch > %f px\n"
346 		"LFT %d ch > %f px (%f)\n"
347 		"RGH %d ch > %f px\n",
348 		glyphs.start_row, rect.top, fFontHeight,
349 		glyphs.end_row, rect.bottom,
350 		glyphs.start_col, rect.left, fFontWidth,
351 		glyphs.end_col, rect.right
352 	);
353 #endif
354 	return rect;
355 }
356 
357 
358 BRect
359 TermView::_GlyphsToPixels(const int width, const int height) const
360 {
361 	VTermRect rect;
362 	rect.start_row = 0;
363 	rect.start_col = 0;
364 	rect.end_row = height;
365 	rect.end_col = width;
366 	return _GlyphsToPixels(rect);
367 }
368 
369 
370 void
371 TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
372 {
373 	// First handle cells from the normal screen
374 	if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
375 		return;
376 
377 	// Try the scroll-back buffer
378 	if (pos.row < 0 && pos.col >= 0) {
379 		int offset = - pos.row - 1;
380 		ScrollBufferItem* line
381 			= (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
382 		if (line != NULL && pos.col < line->cols) {
383 			cell = line->cells[pos.col];
384 			return;
385 		}
386 	}
387 
388 	// All cells outside the used terminal area are drawn with the same
389 	// background color as the top-left one.
390 	// TODO should they use the attributes of the closest neighbor instead?
391 	VTermPos firstPos;
392 	firstPos.row = 0;
393 	firstPos.col = 0;
394 	vterm_screen_get_cell(fTermScreen, firstPos, &cell);
395 	cell.chars[0] = 0;
396 	cell.width = 1;
397 }
398 
399 
400 void
401 TermView::_Damage(VTermRect rect)
402 {
403 	Invalidate(_GlyphsToPixels(rect));
404 }
405 
406 
407 void
408 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
409 {
410 	VTermRect r;
411 
412 	// We need to erase the cursor from its old position
413 	r.start_row = oldPos.row;
414 	r.start_col = oldPos.col;
415 	r.end_col = oldPos.col + 1;
416 	r.end_row = oldPos.row + 1;
417 	Invalidate(_GlyphsToPixels(r));
418 
419 	// And we need to draw it at the new one
420 	r.start_row = pos.row;
421 	r.start_col = pos.col;
422 	r.end_col = pos.col + 1;
423 	r.end_row = pos.row + 1;
424 	Invalidate(_GlyphsToPixels(r));
425 }
426 
427 
428 void
429 TermView::_PushLine(int cols, const VTermScreenCell* cells)
430 {
431 	ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
432 		+ cols * sizeof(VTermScreenCell));
433 	item->cols = cols;
434 	memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
435 
436 	fScrollBuffer.AddItem(item, 0);
437 
438 	free(fScrollBuffer.RemoveItem(kScrollBackSize));
439 
440 	int availableRows, availableCols;
441 	vterm_get_size(fTerm, &availableRows, &availableCols);
442 
443 	VTermRect dirty;
444 	dirty.start_col = 0;
445 	dirty.end_col = availableCols;
446 	dirty.end_row = 0;
447 	dirty.start_row = -fScrollBuffer.CountItems();
448 	// FIXME we should rather use CopyRect if possible, and only invalidate the
449 	// newly exposed area here.
450 	Invalidate(_GlyphsToPixels(dirty));
451 
452 	BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
453 	if (scrollBar != NULL) {
454 		float range = (fScrollBuffer.CountItems() + availableRows)
455 			* fFontHeight;
456 		scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
457 		// TODO we need to adjust this in FrameResized, as availableRows can
458 		// change
459 		scrollBar->SetProportion(availableRows * fFontHeight / range);
460 		scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
461 	}
462 }
463 
464 
465 /* static */ int
466 TermView::_Damage(VTermRect rect, void* user)
467 {
468 	TermView* view = (TermView*)user;
469 	view->_Damage(rect);
470 
471 	return 0;
472 }
473 
474 
475 /* static */ int
476 TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
477 {
478 	TermView* view = (TermView*)user;
479 	view->_MoveCursor(pos, oldPos, visible);
480 
481 	return 0;
482 }
483 
484 
485 /* static */ int
486 TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
487 {
488 	TermView* view = (TermView*)user;
489 	view->_PushLine(cols, cells);
490 
491 	return 0;
492 }
493 
494 
495 const
496 VTermScreenCallbacks TermView::sScreenCallbacks = {
497 	&TermView::_Damage,
498 	/*.moverect =*/ NULL,
499 	&TermView::_MoveCursor,
500 	/*.settermprop =*/ NULL,
501 	/*.setmousefunc =*/ NULL,
502 	/*.bell =*/ NULL,
503 	/*.resize =*/ NULL,
504 	&TermView::_PushLine,
505 	/*.sb_popline =*/ NULL,
506 };
507