haiku/src/apps/serialconnect/TermView.cpp

562 lines
12 KiB
C++

/*
* Copyright 2012-2019, Adrien Destugues, pulkomandy@pulkomandy.tk
* Distributed under the terms of the MIT licence.
*/
#include "TermView.h"
#include <stdio.h>
#include <Entry.h>
#include <File.h>
#include <Font.h>
#include <Layout.h>
#include <ScrollBar.h>
#include "SerialApp.h"
#include "libvterm/src/vterm_internal.h"
struct ScrollBufferItem {
int cols;
VTermScreenCell cells[];
};
TermView::TermView()
:
BView("TermView", B_WILL_DRAW | B_FRAME_EVENTS)
{
_Init();
}
TermView::TermView(BRect r)
:
BView(r, "TermView", B_FOLLOW_ALL_SIDES, B_WILL_DRAW | B_FRAME_EVENTS)
{
_Init();
}
TermView::~TermView()
{
vterm_free(fTerm);
}
void
TermView::AttachedToWindow()
{
BView::AttachedToWindow();
MakeFocus();
}
void
TermView::Draw(BRect updateRect)
{
VTermRect updatedChars = _PixelsToGlyphs(updateRect);
VTermPos pos;
font_height height;
GetFontHeight(&height);
VTermPos cursorPos;
vterm_state_get_cursorpos(vterm_obtain_state(fTerm), &cursorPos);
for (pos.row = updatedChars.start_row; pos.row <= updatedChars.end_row;
pos.row++) {
int x = updatedChars.start_col * fFontWidth + kBorderSpacing;
int y = pos.row * fFontHeight + (int)ceil(height.ascent)
+ kBorderSpacing;
MovePenTo(x, y);
BString string;
VTermScreenCell cell;
int width = 0;
bool isCursor = false;
pos.col = updatedChars.start_col;
_GetCell(pos, cell);
for (pos.col = updatedChars.start_col;
pos.col <= updatedChars.end_col;) {
VTermScreenCell newCell;
_GetCell(pos, newCell);
// We need to start a new extent if:
// - The attributes change
// - The colors change
// - The end of line is reached
// - The current cell is under the cursor
// - The current cell is right of the cursor
if (*(uint32_t*)&cell.attrs != *(uint32_t*)&newCell.attrs
|| !vterm_color_equal(cell.fg, newCell.fg)
|| !vterm_color_equal(cell.bg, newCell.bg)
|| pos.col >= updatedChars.end_col
|| (pos.col == cursorPos.col && pos.row == cursorPos.row)
|| (pos.col == cursorPos.col + 1 && pos.row == cursorPos.row)) {
rgb_color foreground, background;
foreground.red = cell.fg.red;
foreground.green = cell.fg.green;
foreground.blue = cell.fg.blue;
foreground.alpha = 255;
background.red = cell.bg.red;
background.green = cell.bg.green;
background.blue = cell.bg.blue;
background.alpha = 255;
// Draw the cursor by swapping foreground and background colors
if (isCursor ^ cell.attrs.reverse) {
SetLowColor(foreground);
SetViewColor(foreground);
SetHighColor(background);
} else {
SetLowColor(background);
SetViewColor(background);
SetHighColor(foreground);
}
FillRect(BRect(x, y - ceil(height.ascent) + 1,
x + width * fFontWidth - 1,
y + ceil(height.descent) + ceil(height.leading)),
B_SOLID_LOW);
BFont font = be_fixed_font;
if (cell.attrs.bold)
font.SetFace(B_BOLD_FACE);
if (cell.attrs.underline)
font.SetFace(B_UNDERSCORE_FACE);
if (cell.attrs.italic)
font.SetFace(B_ITALIC_FACE);
if (cell.attrs.blink) // FIXME make it actually blink
font.SetFace(B_OUTLINED_FACE);
#if 0
// FIXME B_NEGATIVE_FACE isn't actually implemented so we
// instead swap the colors above
if (cell.attrs.reverse)
font.SetFace(B_NEGATIVE_FACE);
#endif
if (cell.attrs.strike)
font.SetFace(B_STRIKEOUT_FACE);
// TODO handle "font" (alternate fonts), dwl and dhl (double size)
SetFont(&font);
DrawString(string);
x += width * fFontWidth;
// Prepare for next cell
cell = newCell;
string = "";
width = 0;
}
if (pos.col == cursorPos.col && pos.row == cursorPos.row)
isCursor = true;
else
isCursor = false;
if (newCell.chars[0] == 0) {
string += " ";
pos.col ++;
width += 1;
} else {
char buffer[VTERM_MAX_CHARS_PER_CELL];
wcstombs(buffer, (wchar_t*)newCell.chars,
VTERM_MAX_CHARS_PER_CELL);
string += buffer;
width += newCell.width;
pos.col += newCell.width;
}
}
}
}
void
TermView::FrameResized(float width, float height)
{
VTermRect newSize = _PixelsToGlyphs(BRect(0, 0, width - 2 * kBorderSpacing,
height - 2 * kBorderSpacing));
vterm_set_size(fTerm, newSize.end_row, newSize.end_col);
}
void
TermView::GetPreferredSize(float* width, float* height)
{
if (width != NULL)
*width = kDefaultWidth * fFontWidth + 2 * kBorderSpacing - 1;
if (height != NULL)
*height = kDefaultHeight * fFontHeight + 2 * kBorderSpacing - 1;
}
void
TermView::KeyDown(const char* bytes, int32 numBytes)
{
// Translate some keys to more usual VT100 escape codes
switch (bytes[0]) {
case B_UP_ARROW:
numBytes = 3;
bytes = "\x1B[A";
break;
case B_DOWN_ARROW:
numBytes = 3;
bytes = "\x1B[B";
break;
case B_RIGHT_ARROW:
numBytes = 3;
bytes = "\x1B[C";
break;
case B_LEFT_ARROW:
numBytes = 3;
bytes = "\x1B[D";
break;
case B_BACKSPACE:
numBytes = 1;
bytes = "\x7F";
break;
case '\n':
numBytes = fLineTerminator.Length();
bytes = fLineTerminator.String();
break;
}
// Send the bytes to the serial port
BMessage* keyEvent = new BMessage(kMsgDataWrite);
keyEvent->AddData("data", B_RAW_TYPE, bytes, numBytes);
be_app_messenger.SendMessage(keyEvent);
}
void
TermView::MessageReceived(BMessage* message)
{
switch (message->what)
{
case 'DATA':
{
entry_ref ref;
if (message->FindRef("refs", &ref) == B_OK)
{
// The user just dropped a file on us
// TODO send it by XMODEM or so
}
break;
}
default:
BView::MessageReceived(message);
}
}
void
TermView::SetLineTerminator(BString terminator)
{
fLineTerminator = terminator;
}
void
TermView::PushBytes(const char* bytes, size_t length)
{
vterm_push_bytes(fTerm, bytes, length);
}
void
TermView::Clear()
{
while (fScrollBuffer.ItemAt(0)) {
free(fScrollBuffer.RemoveItem((int32)0));
}
vterm_state_reset(vterm_obtain_state(fTerm), 1);
vterm_screen_reset(fTermScreen, 1);
_UpdateScrollbar();
}
// #pragma mark -
void
TermView::_Init()
{
SetFont(be_fixed_font);
font_height height;
GetFontHeight(&height);
fFontHeight = (int)(ceilf(height.ascent) + ceilf(height.descent)
+ ceilf(height.leading));
fFontWidth = (int)be_fixed_font->StringWidth("X");
fTerm = vterm_new(kDefaultHeight, kDefaultWidth);
fTermScreen = vterm_obtain_screen(fTerm);
vterm_screen_set_callbacks(fTermScreen, &sScreenCallbacks, this);
vterm_screen_reset(fTermScreen, 1);
vterm_parser_set_utf8(fTerm, 1);
VTermScreenCell cell;
VTermPos firstPos;
firstPos.row = 0;
firstPos.col = 0;
_GetCell(firstPos, cell);
rgb_color background;
background.red = cell.bg.red;
background.green = cell.bg.green;
background.blue = cell.bg.blue;
background.alpha = 255;
SetViewColor(background);
SetLineTerminator("\n");
}
VTermRect
TermView::_PixelsToGlyphs(BRect pixels) const
{
pixels.OffsetBy(-kBorderSpacing, -kBorderSpacing);
VTermRect rect;
rect.start_col = (int)floor(pixels.left / fFontWidth);
rect.end_col = (int)ceil(pixels.right / fFontWidth);
rect.start_row = (int)floor(pixels.top / fFontHeight);
rect.end_row = (int)ceil(pixels.bottom / fFontHeight);
#if 0
printf(
"TOP %d ch < %f px\n"
"BTM %d ch < %f px\n"
"LFT %d ch < %f px\n"
"RGH %d ch < %f px\n",
rect.start_row, pixels.top,
rect.end_row, pixels.bottom,
rect.start_col, pixels.left,
rect.end_col, pixels.right
);
#endif
return rect;
}
BRect TermView::_GlyphsToPixels(const VTermRect& glyphs) const
{
BRect rect;
rect.top = glyphs.start_row * fFontHeight;
rect.bottom = glyphs.end_row * fFontHeight;
rect.left = glyphs.start_col * fFontWidth;
rect.right = glyphs.end_col * fFontWidth;
rect.OffsetBy(kBorderSpacing, kBorderSpacing);
#if 0
printf(
"TOP %d ch > %f px (%f)\n"
"BTM %d ch > %f px\n"
"LFT %d ch > %f px (%f)\n"
"RGH %d ch > %f px\n",
glyphs.start_row, rect.top, fFontHeight,
glyphs.end_row, rect.bottom,
glyphs.start_col, rect.left, fFontWidth,
glyphs.end_col, rect.right
);
#endif
return rect;
}
BRect
TermView::_GlyphsToPixels(const int width, const int height) const
{
VTermRect rect;
rect.start_row = 0;
rect.start_col = 0;
rect.end_row = height;
rect.end_col = width;
return _GlyphsToPixels(rect);
}
void
TermView::_GetCell(VTermPos pos, VTermScreenCell& cell)
{
// First handle cells from the normal screen
if (vterm_screen_get_cell(fTermScreen, pos, &cell) != 0)
return;
// Try the scroll-back buffer
if (pos.row < 0 && pos.col >= 0) {
int offset = - pos.row - 1;
ScrollBufferItem* line
= (ScrollBufferItem*)fScrollBuffer.ItemAt(offset);
if (line != NULL && pos.col < line->cols) {
cell = line->cells[pos.col];
return;
}
}
// All cells outside the used terminal area are drawn with the same
// background color as the top-left one.
// TODO should they use the attributes of the closest neighbor instead?
VTermPos firstPos;
firstPos.row = 0;
firstPos.col = 0;
vterm_screen_get_cell(fTermScreen, firstPos, &cell);
cell.chars[0] = 0;
cell.width = 1;
}
void
TermView::_Damage(VTermRect rect)
{
Invalidate(_GlyphsToPixels(rect));
}
void
TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible)
{
VTermRect r;
// We need to erase the cursor from its old position
r.start_row = oldPos.row;
r.start_col = oldPos.col;
r.end_col = oldPos.col + 1;
r.end_row = oldPos.row + 1;
Invalidate(_GlyphsToPixels(r));
// And we need to draw it at the new one
r.start_row = pos.row;
r.start_col = pos.col;
r.end_col = pos.col + 1;
r.end_row = pos.row + 1;
Invalidate(_GlyphsToPixels(r));
}
void
TermView::_PushLine(int cols, const VTermScreenCell* cells)
{
ScrollBufferItem* item = (ScrollBufferItem*)malloc(sizeof(int)
+ cols * sizeof(VTermScreenCell));
item->cols = cols;
memcpy(item->cells, cells, cols * sizeof(VTermScreenCell));
fScrollBuffer.AddItem(item, 0);
// Remove extra items if the scrollback gets too long
free(fScrollBuffer.RemoveItem(kScrollBackSize));
_UpdateScrollbar();
}
void
TermView::_UpdateScrollbar()
{
int availableRows, availableCols;
vterm_get_size(fTerm, &availableRows, &availableCols);
VTermRect dirty;
dirty.start_col = 0;
dirty.end_col = availableCols;
dirty.end_row = 0;
dirty.start_row = -fScrollBuffer.CountItems();
// FIXME we should rather use CopyRect if possible, and only invalidate the
// newly exposed area here.
Invalidate(_GlyphsToPixels(dirty));
BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
if (scrollBar != NULL) {
float range = (fScrollBuffer.CountItems() + availableRows)
* fFontHeight;
scrollBar->SetRange(availableRows * fFontHeight - range, 0.0f);
// TODO we need to adjust this in FrameResized, as availableRows can
// change
scrollBar->SetProportion(availableRows * fFontHeight / range);
scrollBar->SetSteps(fFontHeight, fFontHeight * 3);
}
}
int
TermView::_PopLine(int cols, VTermScreenCell* cells)
{
ScrollBufferItem* item =
(ScrollBufferItem*)fScrollBuffer.RemoveItem((int32)0);
if (item == NULL)
return 0;
_UpdateScrollbar();
if (item->cols >= cols) {
memcpy(cells, item->cells, cols * sizeof(VTermScreenCell));
} else {
memcpy(cells, item->cells, item->cols * sizeof(VTermScreenCell));
for (int i = item->cols; i < cols; i++)
cells[i] = cells[i - 1];
}
free(item);
return 1;
}
/* static */ int
TermView::_Damage(VTermRect rect, void* user)
{
TermView* view = (TermView*)user;
view->_Damage(rect);
return 0;
}
/* static */ int
TermView::_MoveCursor(VTermPos pos, VTermPos oldPos, int visible, void* user)
{
TermView* view = (TermView*)user;
view->_MoveCursor(pos, oldPos, visible);
return 0;
}
/* static */ int
TermView::_PushLine(int cols, const VTermScreenCell* cells, void* user)
{
TermView* view = (TermView*)user;
view->_PushLine(cols, cells);
return 0;
}
/* static */ int
TermView::_PopLine(int cols, VTermScreenCell* cells, void* user)
{
TermView* view = (TermView*)user;
return view->_PopLine(cols, cells);
}
const
VTermScreenCallbacks TermView::sScreenCallbacks = {
&TermView::_Damage,
/*.moverect =*/ NULL,
&TermView::_MoveCursor,
/*.settermprop =*/ NULL,
/*.setmousefunc =*/ NULL,
/*.bell =*/ NULL,
/*.resize =*/ NULL,
&TermView::_PushLine,
&TermView::_PopLine,
};