LibVT+Terminal: Implement line wrapping
This commit implements line wrapping in the terminal, and tries its best to move the cursor to the "correct" position.
This commit is contained in:
parent
424965954f
commit
2f2b7814d1
Notes:
sideshowbarker
2024-07-18 11:37:17 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/2f2b7814d15 Pull-request: https://github.com/SerenityOS/serenity/pull/8142 Issue: https://github.com/SerenityOS/serenity/issues/287 Reviewed-by: https://github.com/awesomekling
5 changed files with 206 additions and 22 deletions
|
@ -10,19 +10,101 @@ namespace VT {
|
|||
|
||||
Line::Line(size_t length)
|
||||
{
|
||||
set_length(length);
|
||||
set_length(length, nullptr, nullptr);
|
||||
}
|
||||
|
||||
Line::~Line()
|
||||
{
|
||||
}
|
||||
|
||||
void Line::set_length(size_t new_length)
|
||||
void Line::set_length(size_t new_length, Line* next_line, CursorPosition* cursor, bool cursor_is_on_next_line)
|
||||
{
|
||||
size_t old_length = length();
|
||||
if (old_length == new_length)
|
||||
return;
|
||||
m_cells.resize(new_length);
|
||||
|
||||
// Drop the empty cells
|
||||
if (m_terminated_at.has_value())
|
||||
m_cells.remove(m_terminated_at.value(), m_cells.size() - m_terminated_at.value());
|
||||
|
||||
if (!next_line)
|
||||
return m_cells.resize(new_length);
|
||||
|
||||
if (old_length < new_length)
|
||||
take_cells_from_next_line(old_length, new_length, next_line, cursor_is_on_next_line, cursor);
|
||||
else
|
||||
push_cells_into_next_line(old_length, new_length, next_line, cursor_is_on_next_line, cursor);
|
||||
|
||||
m_cells.resize(max(new_length, static_cast<size_t>(m_terminated_at.value_or(new_length))));
|
||||
}
|
||||
|
||||
void Line::push_cells_into_next_line(size_t old_length, size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor)
|
||||
{
|
||||
// Push as many cells as _wouldn't_ fit into the next line.
|
||||
auto cells_to_preserve = !next_line->m_terminated_at.has_value() && next_line->is_empty() ? 0 : m_terminated_at.value_or(0);
|
||||
auto cells_to_push_into_next_line = min(old_length - new_length, length() - cells_to_preserve);
|
||||
if (!cells_to_push_into_next_line)
|
||||
return;
|
||||
|
||||
auto preserved_cells = length() - cells_to_push_into_next_line;
|
||||
if (next_line->m_terminated_at.has_value())
|
||||
next_line->m_terminated_at = next_line->m_terminated_at.value() + cells_to_push_into_next_line;
|
||||
|
||||
if (m_terminated_at.has_value() && cells_to_preserve == 0) {
|
||||
m_terminated_at.clear();
|
||||
if (!next_line->m_terminated_at.has_value())
|
||||
next_line->m_terminated_at = cells_to_push_into_next_line;
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (cursor_is_on_next_line) {
|
||||
cursor->column += cells_to_push_into_next_line;
|
||||
} else if (cursor->column >= preserved_cells) {
|
||||
cursor->row++;
|
||||
cursor->column = cursor->column - preserved_cells;
|
||||
}
|
||||
}
|
||||
|
||||
next_line->m_cells.prepend(m_cells.span().slice_from_end(cells_to_push_into_next_line).data(), cells_to_push_into_next_line);
|
||||
m_cells.remove(m_cells.size() - cells_to_push_into_next_line, cells_to_push_into_next_line);
|
||||
if (m_terminated_at.has_value())
|
||||
m_terminated_at = m_terminated_at.value() - cells_to_push_into_next_line;
|
||||
}
|
||||
|
||||
void Line::take_cells_from_next_line(size_t old_length, size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor)
|
||||
{
|
||||
// Take as many cells as would fit from the next line
|
||||
if (m_terminated_at.has_value())
|
||||
return;
|
||||
|
||||
auto cells_to_grab_from_next_line = min(new_length - old_length, next_line->length());
|
||||
auto clear_next_line = false;
|
||||
if (next_line->m_terminated_at.has_value()) {
|
||||
cells_to_grab_from_next_line = min(cells_to_grab_from_next_line, static_cast<size_t>(next_line->m_terminated_at.value()));
|
||||
if (cells_to_grab_from_next_line == *next_line->m_terminated_at) {
|
||||
m_terminated_at = old_length + *next_line->m_terminated_at;
|
||||
next_line->m_terminated_at.clear();
|
||||
clear_next_line = true;
|
||||
} else {
|
||||
next_line->m_terminated_at = next_line->m_terminated_at.value() - cells_to_grab_from_next_line;
|
||||
}
|
||||
}
|
||||
|
||||
if (cells_to_grab_from_next_line) {
|
||||
if (cursor && cursor_is_on_next_line) {
|
||||
if (cursor->column <= cells_to_grab_from_next_line) {
|
||||
cursor->row--;
|
||||
cursor->column += m_cells.size();
|
||||
} else {
|
||||
cursor->column -= cells_to_grab_from_next_line;
|
||||
}
|
||||
}
|
||||
m_cells.append(next_line->m_cells.data(), cells_to_grab_from_next_line);
|
||||
next_line->m_cells.remove(0, cells_to_grab_from_next_line);
|
||||
}
|
||||
|
||||
if (clear_next_line)
|
||||
next_line->m_cells.clear();
|
||||
}
|
||||
|
||||
void Line::clear_range(size_t first_column, size_t last_column, const Attribute& attribute)
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <AK/String.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibVT/Attribute.h>
|
||||
#include <LibVT/Position.h>
|
||||
#include <LibVT/XtermColors.h>
|
||||
|
||||
namespace VT {
|
||||
|
@ -25,6 +26,8 @@ public:
|
|||
struct Cell {
|
||||
u32 code_point { ' ' };
|
||||
Attribute attribute;
|
||||
|
||||
bool operator!=(Cell const& other) const { return code_point != other.code_point || attribute != other.attribute; }
|
||||
};
|
||||
|
||||
const Attribute& attribute_at(size_t index) const { return m_cells[index].attribute; }
|
||||
|
@ -41,8 +44,16 @@ public:
|
|||
void clear_range(size_t first_column, size_t last_column, const Attribute& attribute = Attribute());
|
||||
bool has_only_one_background_color() const;
|
||||
|
||||
size_t length() const { return m_cells.size(); }
|
||||
void set_length(size_t);
|
||||
bool is_empty() const
|
||||
{
|
||||
return !any_of(m_cells.begin(), m_cells.end(), [](auto& cell) { return cell != Cell(); });
|
||||
}
|
||||
|
||||
size_t length() const
|
||||
{
|
||||
return m_cells.size();
|
||||
}
|
||||
void set_length(size_t, Line* next_line, CursorPosition* cursor, bool cursor_is_on_next_line = true);
|
||||
|
||||
u32 code_point(size_t index) const
|
||||
{
|
||||
|
@ -67,6 +78,9 @@ public:
|
|||
void set_terminated(u16 column) { m_terminated_at = column; }
|
||||
|
||||
private:
|
||||
void take_cells_from_next_line(size_t old_length, size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor);
|
||||
void push_cells_into_next_line(size_t old_length, size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor);
|
||||
|
||||
Vector<Cell> m_cells;
|
||||
bool m_dirty { false };
|
||||
// Note: The alignment is 8, so this member lives in the padding (that already existed before it was introduced)
|
||||
|
|
|
@ -51,4 +51,15 @@ private:
|
|||
int m_column { -1 };
|
||||
};
|
||||
|
||||
struct CursorPosition {
|
||||
u16 row { 0 };
|
||||
u16 column { 0 };
|
||||
|
||||
void clamp(u16 max_row, u16 max_column)
|
||||
{
|
||||
row = min(row, max_row);
|
||||
column = min(column, max_column);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include "Terminal.h"
|
||||
#include <AK/Debug.h>
|
||||
#include <AK/Queue.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <AK/TemporaryChange.h>
|
||||
|
@ -1439,6 +1440,95 @@ void Terminal::set_size(u16 columns, u16 rows)
|
|||
if (columns == m_columns && rows == m_rows)
|
||||
return;
|
||||
|
||||
// If we're making the terminal larger (column-wise), start at the end and go up, taking cells from the line below.
|
||||
// otherwise start at the beginning and go down, pushing cells into the line below.
|
||||
auto resize_and_rewrap = [&](auto& buffer, auto& old_cursor) {
|
||||
auto cursor_on_line = [&](auto index) {
|
||||
return index == old_cursor.row ? &old_cursor : nullptr;
|
||||
};
|
||||
// Two passes, one from top to bottom, another from bottom to top
|
||||
for (size_t pass = 0; pass < 2; ++pass) {
|
||||
auto forwards = (pass == 0) ^ (columns < m_columns);
|
||||
if (forwards) {
|
||||
for (size_t i = 1; i <= buffer.size(); ++i) {
|
||||
auto is_at_seam = i == 1;
|
||||
auto next_line = is_at_seam ? nullptr : &buffer[buffer.size() - i + 1];
|
||||
auto& line = buffer[buffer.size() - i];
|
||||
auto next_cursor = cursor_on_line(buffer.size() - i + 1);
|
||||
line.set_length(columns, next_line, next_cursor ?: cursor_on_line(buffer.size() - i), !!next_cursor);
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < buffer.size(); ++i) {
|
||||
auto is_at_seam = i + 1 == buffer.size();
|
||||
auto next_line = is_at_seam ? nullptr : &buffer[i + 1];
|
||||
auto next_cursor = cursor_on_line(i + 1);
|
||||
buffer[i].set_length(columns, next_line, next_cursor ?: cursor_on_line(i), !!next_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Queue<size_t> lines_to_reevaluate;
|
||||
for (size_t i = 0; i < buffer.size(); ++i) {
|
||||
if (buffer[i].length() != columns)
|
||||
lines_to_reevaluate.enqueue(i);
|
||||
}
|
||||
size_t rows_inserted = 0;
|
||||
while (!lines_to_reevaluate.is_empty()) {
|
||||
auto index = lines_to_reevaluate.dequeue();
|
||||
auto is_at_seam = index + 1 == buffer.size();
|
||||
auto next_line = is_at_seam ? nullptr : &buffer[index + 1];
|
||||
auto& line = buffer[index];
|
||||
auto next_cursor = cursor_on_line(index + 1);
|
||||
line.set_length(columns, next_line, next_cursor ?: cursor_on_line(index), !!next_cursor);
|
||||
if (line.length() > columns) {
|
||||
auto current_cursor = cursor_on_line(index);
|
||||
// Split the line into two (or more)
|
||||
++index;
|
||||
++rows_inserted;
|
||||
buffer.insert(index, make<Line>(0));
|
||||
VERIFY(buffer[index].length() == 0);
|
||||
line.set_length(columns, &buffer[index], current_cursor, false);
|
||||
// If we inserted a line and the old cursor was after that line, increment its row
|
||||
if (!current_cursor && old_cursor.row >= index)
|
||||
++old_cursor.row;
|
||||
|
||||
if (buffer[index].length() != columns)
|
||||
lines_to_reevaluate.enqueue(index);
|
||||
}
|
||||
if (next_line && next_line->length() != columns)
|
||||
lines_to_reevaluate.enqueue(index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return old_cursor;
|
||||
};
|
||||
|
||||
CursorPosition cursor_tracker { cursor_row(), cursor_column() };
|
||||
resize_and_rewrap(m_normal_screen_buffer, cursor_tracker);
|
||||
if (m_normal_screen_buffer.size() > rows) {
|
||||
if (auto extra_lines = m_normal_screen_buffer.size() - rows) {
|
||||
while (extra_lines > 0) {
|
||||
if (m_normal_screen_buffer.size() <= cursor_tracker.row)
|
||||
break;
|
||||
if (m_normal_screen_buffer.last().is_empty()) {
|
||||
if (m_normal_screen_buffer[m_normal_screen_buffer.size() - 2].termination_column().has_value())
|
||||
break;
|
||||
--extra_lines;
|
||||
m_normal_screen_buffer.take_last();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (size_t i = 0; i < extra_lines; ++i)
|
||||
m_history.append(m_normal_screen_buffer.take_first());
|
||||
m_client.terminal_history_changed(extra_lines);
|
||||
}
|
||||
}
|
||||
|
||||
CursorPosition dummy_cursor_tracker {};
|
||||
resize_and_rewrap(m_alternate_screen_buffer, dummy_cursor_tracker);
|
||||
if (m_alternate_screen_buffer.size() > rows)
|
||||
m_alternate_screen_buffer.remove(0, m_alternate_screen_buffer.size() - rows);
|
||||
|
||||
if (rows > m_rows) {
|
||||
while (m_normal_screen_buffer.size() < rows)
|
||||
m_normal_screen_buffer.append(make<Line>(columns));
|
||||
|
@ -1449,11 +1539,6 @@ void Terminal::set_size(u16 columns, u16 rows)
|
|||
m_alternate_screen_buffer.shrink(rows);
|
||||
}
|
||||
|
||||
for (int i = 0; i < rows; ++i) {
|
||||
m_normal_screen_buffer[i].set_length(columns);
|
||||
m_alternate_screen_buffer[i].set_length(columns);
|
||||
}
|
||||
|
||||
m_columns = columns;
|
||||
m_rows = rows;
|
||||
|
||||
|
@ -1471,6 +1556,8 @@ void Terminal::set_size(u16 columns, u16 rows)
|
|||
// Rightmost column is always last tab on line.
|
||||
m_horizontal_tabs[columns - 1] = 1;
|
||||
|
||||
set_cursor(cursor_tracker.row, cursor_tracker.column);
|
||||
|
||||
m_client.terminal_did_resize(m_columns, m_rows);
|
||||
|
||||
dbgln_if(TERMINAL_DEBUG, "Set terminal size: {}x{}", m_rows, m_columns);
|
||||
|
@ -1480,7 +1567,8 @@ void Terminal::set_size(u16 columns, u16 rows)
|
|||
#ifndef KERNEL
|
||||
void Terminal::invalidate_cursor()
|
||||
{
|
||||
active_buffer()[cursor_row()].set_dirty(true);
|
||||
if (cursor_row() < active_buffer().size())
|
||||
active_buffer()[cursor_row()].set_dirty(true);
|
||||
}
|
||||
|
||||
Attribute Terminal::attribute_at(const Position& position) const
|
||||
|
|
|
@ -194,17 +194,6 @@ protected:
|
|||
virtual void receive_dcs_char(u8 byte) override;
|
||||
virtual void execute_dcs_sequence() override;
|
||||
|
||||
struct CursorPosition {
|
||||
u16 row { 0 };
|
||||
u16 column { 0 };
|
||||
|
||||
void clamp(u16 max_row, u16 max_column)
|
||||
{
|
||||
row = min(row, max_row);
|
||||
column = min(column, max_column);
|
||||
}
|
||||
};
|
||||
|
||||
struct BufferState {
|
||||
Attribute attribute;
|
||||
CursorPosition cursor;
|
||||
|
|
Loading…
Add table
Reference in a new issue