mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-04 01:12:56 +00:00
Previously we'd blit every pixel in every window that intersected any dirty rect to the back buffer. With this patch, we limit ourselves to blitting the pixels inside the actual dirty rects. There's still a lot of optimizations to make in this code.
514 lines
14 KiB
C++
514 lines
14 KiB
C++
#include "Terminal.h"
|
|
#include <AK/AKString.h>
|
|
#include <SharedGraphics/Font.h>
|
|
#include <SharedGraphics/Painter.h>
|
|
#include <AK/StdLibExtras.h>
|
|
#include <unistd.h>
|
|
#include <stdio.h>
|
|
#include <gui.h>
|
|
|
|
#define FAST_SCROLL
|
|
|
|
void Terminal::create_window()
|
|
{
|
|
m_pixel_width = m_columns * font().glyph_width() + m_inset * 2;
|
|
m_pixel_height = (m_rows * (font().glyph_height() + m_line_spacing)) + (m_inset * 2) - m_line_spacing;
|
|
|
|
GUI_WindowParameters params;
|
|
params.rect = { { 300, 300 }, { m_pixel_width, m_pixel_height } };
|
|
params.background_color = 0x000000;
|
|
strcpy(params.title, "Terminal");
|
|
m_window_id = gui_create_window(¶ms);
|
|
ASSERT(m_window_id > 0);
|
|
if (m_window_id < 0) {
|
|
perror("gui_create_window");
|
|
exit(1);
|
|
}
|
|
|
|
GUI_WindowBackingStoreInfo info;
|
|
int rc = gui_get_window_backing_store(m_window_id, &info);
|
|
if (rc < 0) {
|
|
perror("gui_get_window_backing_store");
|
|
exit(1);
|
|
}
|
|
|
|
m_backing = GraphicsBitmap::create_wrapper(info.size, info.pixels);
|
|
dbgprintf("(Terminal:%d) window backing %ux%u @ %p\n", getpid(), info.size.width, info.size.height, info.pixels);
|
|
|
|
}
|
|
|
|
Terminal::Terminal()
|
|
: m_font(Font::default_font())
|
|
{
|
|
m_line_height = font().glyph_height() + m_line_spacing;
|
|
|
|
set_size(80, 25);
|
|
m_horizontal_tabs = static_cast<byte*>(malloc(columns()));
|
|
for (unsigned i = 0; i < columns(); ++i)
|
|
m_horizontal_tabs[i] = (i % 8) == 0;
|
|
// Rightmost column is always last tab on line.
|
|
m_horizontal_tabs[columns() - 1] = 1;
|
|
|
|
m_row_needs_invalidation = (bool*)(malloc(rows() * sizeof(bool)));
|
|
m_buffer = (byte*)malloc(rows() * columns());
|
|
m_attributes = (Attribute*)malloc(rows() * columns() * sizeof(Attribute));
|
|
memset(m_buffer, ' ', m_rows * m_columns);
|
|
for (size_t i = 0; i < rows() * columns(); ++i)
|
|
m_attributes[i].reset();
|
|
}
|
|
|
|
Terminal::~Terminal()
|
|
{
|
|
free(m_row_needs_invalidation);
|
|
free(m_buffer);
|
|
free(m_attributes);
|
|
free(m_horizontal_tabs);
|
|
}
|
|
|
|
void Terminal::clear()
|
|
{
|
|
memset(m_buffer, ' ', m_rows * m_columns);
|
|
for (size_t i = 0; i < rows() * columns(); ++i)
|
|
m_attributes[i].reset();
|
|
set_cursor(0, 0);
|
|
}
|
|
|
|
inline bool is_valid_parameter_character(byte ch)
|
|
{
|
|
return ch >= 0x30 && ch <= 0x3f;
|
|
}
|
|
|
|
inline bool is_valid_intermediate_character(byte ch)
|
|
{
|
|
return ch >= 0x20 && ch <= 0x2f;
|
|
}
|
|
|
|
inline bool is_valid_final_character(byte ch)
|
|
{
|
|
return ch >= 0x40 && ch <= 0x7e;
|
|
}
|
|
|
|
unsigned parseUInt(const String& str, bool& ok)
|
|
{
|
|
unsigned value = 0;
|
|
for (size_t i = 0; i < str.length(); ++i) {
|
|
if (str[i] < '0' || str[i] > '9') {
|
|
ok = false;
|
|
return 0;
|
|
}
|
|
value = value * 10;
|
|
value += str[i] - '0';
|
|
}
|
|
ok = true;
|
|
return value;
|
|
}
|
|
|
|
enum ANSIColor : byte {
|
|
Black = 0,
|
|
Red,
|
|
Green,
|
|
Brown,
|
|
Blue,
|
|
Magenta,
|
|
Cyan,
|
|
LightGray,
|
|
DarkGray,
|
|
BrightRed,
|
|
BrightGreen,
|
|
Yellow,
|
|
BrightBlue,
|
|
BrightMagenta,
|
|
BrightCyan,
|
|
White,
|
|
};
|
|
|
|
static inline Color ansi_color(unsigned color)
|
|
{
|
|
switch (color) {
|
|
case ANSIColor::Black: return Color(0, 0, 0);
|
|
case ANSIColor::Red: return Color(225, 56, 43);
|
|
case ANSIColor::Green: return Color(57, 181, 74);
|
|
case ANSIColor::Brown: return Color(255, 199, 6);
|
|
case ANSIColor::Blue: return Color(0, 111, 184);
|
|
case ANSIColor::Magenta: return Color(118, 38, 113);
|
|
case ANSIColor::Cyan: return Color(44, 181, 233);
|
|
case ANSIColor::LightGray: return Color(204, 204, 204);
|
|
case ANSIColor::DarkGray: return Color(128, 128, 128);
|
|
case ANSIColor::BrightRed: return Color(255, 0, 0);
|
|
case ANSIColor::BrightGreen: return Color(0, 255, 0);
|
|
case ANSIColor::Yellow: return Color(255, 255, 0);
|
|
case ANSIColor::BrightBlue: return Color(0, 0, 255);
|
|
case ANSIColor::BrightMagenta: return Color(255, 0, 255);
|
|
case ANSIColor::BrightCyan: return Color(0, 255, 255);
|
|
case ANSIColor::White: return Color(255, 255, 255);
|
|
}
|
|
ASSERT_NOT_REACHED();
|
|
return Color::White;
|
|
}
|
|
|
|
void Terminal::escape$m(const Vector<unsigned>& params)
|
|
{
|
|
for (auto param : params) {
|
|
switch (param) {
|
|
case 0:
|
|
// Reset
|
|
m_current_attribute.reset();
|
|
break;
|
|
case 1:
|
|
// Bold
|
|
m_current_attribute.bold = true;
|
|
break;
|
|
case 30:
|
|
case 31:
|
|
case 32:
|
|
case 33:
|
|
case 34:
|
|
case 35:
|
|
case 36:
|
|
case 37:
|
|
// Foreground color
|
|
m_current_attribute.foreground_color = param - 30;
|
|
break;
|
|
case 40:
|
|
case 41:
|
|
case 42:
|
|
case 43:
|
|
case 44:
|
|
case 45:
|
|
case 46:
|
|
case 47:
|
|
// Background color
|
|
m_current_attribute.background_color = param - 30;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Terminal::escape$s(const Vector<unsigned>&)
|
|
{
|
|
m_saved_cursor_row = m_cursor_row;
|
|
m_saved_cursor_column = m_cursor_column;
|
|
}
|
|
|
|
void Terminal::escape$u(const Vector<unsigned>&)
|
|
{
|
|
set_cursor(m_saved_cursor_row, m_saved_cursor_column);
|
|
}
|
|
|
|
void Terminal::escape$H(const Vector<unsigned>& params)
|
|
{
|
|
unsigned row = 1;
|
|
unsigned col = 1;
|
|
if (params.size() >= 1)
|
|
row = params[0];
|
|
if (params.size() >= 2)
|
|
col = params[1];
|
|
set_cursor(row - 1, col - 1);
|
|
}
|
|
|
|
void Terminal::escape$A(const Vector<unsigned>& params)
|
|
{
|
|
int num = 1;
|
|
if (params.size() >= 1)
|
|
num = params[0];
|
|
int new_row = (int)m_cursor_row - num;
|
|
if (new_row < 0)
|
|
new_row = 0;
|
|
set_cursor(new_row, m_cursor_column);
|
|
}
|
|
|
|
void Terminal::escape$D(const Vector<unsigned>& params)
|
|
{
|
|
int num = 1;
|
|
if (params.size() >= 1)
|
|
num = params[0];
|
|
int new_column = (int)m_cursor_column - num;
|
|
if (new_column < 0)
|
|
new_column = 0;
|
|
set_cursor(m_cursor_row, new_column);
|
|
}
|
|
|
|
void Terminal::escape$J(const Vector<unsigned>& params)
|
|
{
|
|
int mode = 0;
|
|
if (params.size() >= 1)
|
|
mode = params[0];
|
|
switch (mode) {
|
|
case 0:
|
|
// FIXME: Clear from cursor to end of screen.
|
|
notImplemented();
|
|
break;
|
|
case 1:
|
|
// FIXME: Clear from cursor to beginning of screen.
|
|
notImplemented();
|
|
break;
|
|
case 2:
|
|
clear();
|
|
break;
|
|
case 3:
|
|
// FIXME: <esc>[3J should also clear the scrollback buffer.
|
|
clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Terminal::execute_escape_sequence(byte final)
|
|
{
|
|
auto paramparts = String((const char*)m_parameters.data(), m_parameters.size()).split(';');
|
|
Vector<unsigned> params;
|
|
for (auto& parampart : paramparts) {
|
|
bool ok;
|
|
unsigned value = parseUInt(parampart, ok);
|
|
if (!ok) {
|
|
// FIXME: Should we do something else?
|
|
return;
|
|
}
|
|
params.append(value);
|
|
}
|
|
switch (final) {
|
|
case 'A': escape$A(params); break;
|
|
case 'D': escape$D(params); break;
|
|
case 'H': escape$H(params); break;
|
|
case 'J': escape$J(params); break;
|
|
case 'm': escape$m(params); break;
|
|
case 's': escape$s(params); break;
|
|
case 'u': escape$u(params); break;
|
|
default: break;
|
|
}
|
|
|
|
m_parameters.clear();
|
|
m_intermediates.clear();
|
|
}
|
|
|
|
void Terminal::scroll_up()
|
|
{
|
|
word new_row = m_cursor_row;
|
|
if (m_cursor_row == (rows() - 1)) {
|
|
memcpy(m_buffer, m_buffer + m_columns, m_columns * (m_rows - 1));
|
|
#ifdef FAST_SCROLL
|
|
++m_rows_to_scroll_backing_store;
|
|
#else
|
|
for (size_t i = 0; i < m_rows * m_columns; ++i) {
|
|
m_attributes[i].dirty = true;
|
|
}
|
|
#endif
|
|
memset(&m_buffer[(m_rows - 1) * m_columns], ' ', m_columns);
|
|
// NOTE: We have to invalidate the cursor before memcpy()'ing the attributes.
|
|
invalidate_cursor();
|
|
memcpy(m_attributes, m_attributes + m_columns, m_columns * (m_rows - 1) * sizeof(Attribute));
|
|
for (size_t i = 0; i < m_columns; ++i)
|
|
m_attributes[((m_rows - 1) * m_columns) + i].reset();
|
|
} else {
|
|
++new_row;
|
|
}
|
|
set_cursor(new_row, 0);
|
|
}
|
|
|
|
void Terminal::set_cursor(unsigned row, unsigned column)
|
|
{
|
|
ASSERT(row < rows());
|
|
ASSERT(column < columns());
|
|
invalidate_cursor();
|
|
m_cursor_row = row;
|
|
m_cursor_column = column;
|
|
invalidate_cursor();
|
|
}
|
|
|
|
void Terminal::put_character_at(unsigned row, unsigned column, byte ch)
|
|
{
|
|
ASSERT(row < rows());
|
|
ASSERT(column < columns());
|
|
word cur = (row * m_columns) + (column);
|
|
m_buffer[cur] = ch;
|
|
m_attributes[cur] = m_current_attribute;
|
|
}
|
|
|
|
void Terminal::on_char(byte ch)
|
|
{
|
|
switch (m_escape_state) {
|
|
case ExpectBracket:
|
|
if (ch == '[')
|
|
m_escape_state = ExpectParameter;
|
|
else
|
|
m_escape_state = Normal;
|
|
return;
|
|
case ExpectParameter:
|
|
if (is_valid_parameter_character(ch)) {
|
|
m_parameters.append(ch);
|
|
return;
|
|
}
|
|
m_escape_state = ExpectIntermediate;
|
|
// fall through
|
|
case ExpectIntermediate:
|
|
if (is_valid_intermediate_character(ch)) {
|
|
m_intermediates.append(ch);
|
|
return;
|
|
}
|
|
m_escape_state = ExpectFinal;
|
|
// fall through
|
|
case ExpectFinal:
|
|
if (is_valid_final_character(ch)) {
|
|
m_escape_state = Normal;
|
|
execute_escape_sequence(ch);
|
|
return;
|
|
}
|
|
m_escape_state = Normal;
|
|
return;
|
|
case Normal:
|
|
break;
|
|
}
|
|
|
|
switch (ch) {
|
|
case '\0':
|
|
return;
|
|
case '\033':
|
|
m_escape_state = ExpectBracket;
|
|
return;
|
|
case 8: // Backspace
|
|
if (m_cursor_column) {
|
|
set_cursor(m_cursor_row, m_cursor_column - 1);
|
|
put_character_at(m_cursor_row, m_cursor_column, ' ');
|
|
return;
|
|
}
|
|
break;
|
|
case '\a':
|
|
// FIXME: Bell!
|
|
return;
|
|
case '\t': {
|
|
for (unsigned i = m_cursor_column; i < columns(); ++i) {
|
|
if (m_horizontal_tabs[i]) {
|
|
set_cursor(m_cursor_row, i);
|
|
return;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case '\n':
|
|
scroll_up();
|
|
return;
|
|
}
|
|
|
|
put_character_at(m_cursor_row, m_cursor_column, ch);
|
|
|
|
auto new_column = m_cursor_column + 1;
|
|
if (new_column < columns())
|
|
set_cursor(m_cursor_row, new_column);
|
|
else
|
|
scroll_up();
|
|
}
|
|
|
|
void Terminal::set_size(word columns, word rows)
|
|
{
|
|
m_columns = columns;
|
|
m_rows = rows;
|
|
}
|
|
|
|
Rect Terminal::glyph_rect(word row, word column)
|
|
{
|
|
int y = row * m_line_height;
|
|
int x = column * font().glyph_width();
|
|
return { x + m_inset, y + m_inset, font().glyph_width(), font().glyph_height() };
|
|
}
|
|
|
|
Rect Terminal::row_rect(word row)
|
|
{
|
|
int y = row * m_line_height;
|
|
return { m_inset, y + m_inset, font().glyph_width() * m_columns, font().glyph_height() };
|
|
}
|
|
|
|
inline Terminal::Attribute& Terminal::attribute_at(word row, word column)
|
|
{
|
|
return m_attributes[(row * m_columns) + column];
|
|
}
|
|
|
|
void Terminal::paint()
|
|
{
|
|
Rect rect { 0, 0, m_pixel_width, m_pixel_height };
|
|
Painter painter(*m_backing);
|
|
|
|
memset(m_row_needs_invalidation, 0, rows() * sizeof(bool));
|
|
|
|
#ifdef FAST_SCROLL
|
|
if (m_rows_to_scroll_backing_store && m_rows_to_scroll_backing_store < m_rows) {
|
|
int first_scanline = m_inset;
|
|
int second_scanline = m_inset + (m_rows_to_scroll_backing_store * m_line_height);
|
|
int num_rows_to_memcpy = m_rows - m_rows_to_scroll_backing_store;
|
|
int scanlines_to_copy = (num_rows_to_memcpy * m_line_height) - m_line_spacing;
|
|
fast_dword_copy(
|
|
m_backing->scanline(first_scanline),
|
|
m_backing->scanline(second_scanline),
|
|
scanlines_to_copy * m_pixel_width
|
|
);
|
|
m_need_full_invalidation = true;
|
|
attribute_at(m_cursor_row - m_rows_to_scroll_backing_store, m_cursor_column).dirty = true;
|
|
}
|
|
m_rows_to_scroll_backing_store = 0;
|
|
#endif
|
|
|
|
for (word row = 0; row < m_rows; ++row) {
|
|
for (word column = 0; column < m_columns; ++column) {
|
|
auto& attribute = attribute_at(row, column);
|
|
if (!attribute.dirty)
|
|
continue;
|
|
attribute.dirty = false;
|
|
m_row_needs_invalidation[row] = true;
|
|
char ch = m_buffer[(row * m_columns) + (column)];
|
|
auto character_rect = glyph_rect(row, column);
|
|
auto character_background = ansi_color(attribute.background_color);
|
|
painter.fill_rect(character_rect, character_background);
|
|
if (ch == ' ')
|
|
continue;
|
|
painter.draw_glyph(character_rect.location(), ch, ansi_color(attribute.foreground_color));
|
|
}
|
|
}
|
|
|
|
auto cursor_rect = glyph_rect(m_cursor_row, m_cursor_column);
|
|
if (m_in_active_window)
|
|
painter.fill_rect(cursor_rect, Color::MidGray);
|
|
else
|
|
painter.draw_rect(cursor_rect, Color::MidGray);
|
|
|
|
m_row_needs_invalidation[m_cursor_row] = true;
|
|
|
|
if (m_belling) {
|
|
m_need_full_invalidation = true;
|
|
painter.draw_rect(rect, Color::Red);
|
|
}
|
|
|
|
if (m_need_full_invalidation) {
|
|
invalidate_window();
|
|
m_need_full_invalidation = false;
|
|
return;
|
|
}
|
|
|
|
Rect invalidation_rect;
|
|
for (int i = 0; i < m_rows; ++i) {
|
|
if (m_row_needs_invalidation[i])
|
|
invalidation_rect = invalidation_rect.united(row_rect(i));
|
|
}
|
|
invalidate_window(invalidation_rect);
|
|
}
|
|
|
|
void Terminal::invalidate_window(const Rect& a_rect)
|
|
{
|
|
GUI_Rect rect = a_rect;
|
|
int rc = gui_invalidate_window(m_window_id, a_rect.is_null() ? nullptr : &rect);
|
|
if (rc < 0) {
|
|
perror("gui_invalidate_window");
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
void Terminal::set_in_active_window(bool b)
|
|
{
|
|
if (m_in_active_window == b)
|
|
return;
|
|
m_in_active_window = b;
|
|
invalidate_cursor();
|
|
paint();
|
|
}
|
|
|
|
void Terminal::invalidate_cursor()
|
|
{
|
|
attribute_at(m_cursor_row, m_cursor_column).dirty = true;
|
|
}
|