LibGUI: Move GTextDocument out of GTextEditor

The idea here is to decouple the document from the editor widget so you
could have multiple editors being views onto the same document.

This doesn't work yet, since the document and editor are coupled in
various ways still (including a per-line back-pointer to the editor.)
This commit is contained in:
Andreas Kling 2019-10-27 16:10:07 +01:00
parent 1bcbc3f827
commit f1c6193d6d
Notes: sideshowbarker 2024-07-19 11:31:13 +09:00
6 changed files with 302 additions and 240 deletions

View file

@ -286,12 +286,12 @@ static void rehighlight()
CppLexer lexer(text);
auto tokens = lexer.lex();
Vector<GTextEditor::Span> spans;
Vector<GTextDocumentSpan> spans;
for (auto& token : tokens) {
#ifdef DEBUG_SYNTAX_HIGHLIGHTING
dbg() << token.to_string() << " @ " << token.m_start.line << ":" << token.m_start.column << " - " << token.m_end.line << ":" << token.m_end.column;
#endif
GTextEditor::Span span;
GTextDocumentSpan span;
span.range.set_start({ token.m_start.line, token.m_start.column });
span.range.set_end({ token.m_end.line, token.m_end.column });
auto style = style_for_token_type(token.m_type);
@ -299,7 +299,7 @@ static void rehighlight()
span.font = style.font;
spans.append(span);
}
current_editor().set_spans(spans);
current_editor().document().set_spans(spans);
current_editor().update();
}

View file

@ -0,0 +1,113 @@
#include <LibGUI/GTextDocument.h>
#include <ctype.h>
GTextDocument::GTextDocument(GTextEditor& editor)
: m_editor(editor)
{
m_lines.append(make<GTextDocumentLine>(m_editor));
}
void GTextDocument::set_text(const StringView& text)
{
m_spans.clear();
m_lines.clear();
int start_of_current_line = 0;
auto add_line = [&](int current_position) {
int line_length = current_position - start_of_current_line;
auto line = make<GTextDocumentLine>(m_editor);
if (line_length)
line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line));
m_lines.append(move(line));
start_of_current_line = current_position + 1;
};
int i = 0;
for (i = 0; i < text.length(); ++i) {
if (text[i] == '\n')
add_line(i);
}
add_line(i);
}
int GTextDocumentLine::first_non_whitespace_column() const
{
for (int i = 0; i < length(); ++i) {
if (!isspace(m_text[i]))
return i;
}
return length();
}
GTextDocumentLine::GTextDocumentLine(GTextEditor& editor)
: m_editor(editor)
{
clear();
}
GTextDocumentLine::GTextDocumentLine(GTextEditor& editor, const StringView& text)
: m_editor(editor)
{
set_text(text);
}
void GTextDocumentLine::clear()
{
m_text.clear();
m_text.append(0);
}
void GTextDocumentLine::set_text(const StringView& text)
{
if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length()))
return;
if (text.is_empty()) {
clear();
return;
}
m_text.resize(text.length() + 1);
memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1);
}
void GTextDocumentLine::append(const char* characters, int length)
{
int old_length = m_text.size() - 1;
m_text.resize(m_text.size() + length);
memcpy(m_text.data() + old_length, characters, length);
m_text.last() = 0;
}
void GTextDocumentLine::append(char ch)
{
insert(length(), ch);
}
void GTextDocumentLine::prepend(char ch)
{
insert(0, ch);
}
void GTextDocumentLine::insert(int index, char ch)
{
if (index == length()) {
m_text.last() = ch;
m_text.append(0);
} else {
m_text.insert(index, move(ch));
}
}
void GTextDocumentLine::remove(int index)
{
if (index == length()) {
m_text.take_last();
m_text.last() = 0;
} else {
m_text.remove(index);
}
}
void GTextDocumentLine::truncate(int length)
{
m_text.resize(length + 1);
m_text.last() = 0;
}

View file

@ -0,0 +1,83 @@
#pragma once
#include <AK/NonnullOwnPtrVector.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <LibDraw/Color.h>
#include <LibDraw/Font.h>
#include <LibGUI/GTextRange.h>
class GTextEditor;
class GTextDocumentLine;
struct GTextDocumentSpan {
GTextRange range;
Color color;
const Font* font { nullptr };
};
class GTextDocument : public RefCounted<GTextDocument> {
public:
static NonnullRefPtr<GTextDocument> create(GTextEditor& editor)
{
return adopt(*new GTextDocument(editor));
}
int line_count() const { return m_lines.size(); }
const GTextDocumentLine& line(int line_index) const { return m_lines[line_index]; }
GTextDocumentLine& line(int line_index) { return m_lines[line_index]; }
void set_spans(const Vector<GTextDocumentSpan>& spans) { m_spans = spans; }
void set_text(const StringView&);
const NonnullOwnPtrVector<GTextDocumentLine>& lines() const { return m_lines; }
NonnullOwnPtrVector<GTextDocumentLine>& lines() { return m_lines; }
bool has_spans() const { return !m_spans.is_empty(); }
const Vector<GTextDocumentSpan>& spans() const { return m_spans; }
private:
explicit GTextDocument(GTextEditor&);
NonnullOwnPtrVector<GTextDocumentLine> m_lines;
Vector<GTextDocumentSpan> m_spans;
GTextEditor& m_editor;
};
class GTextDocumentLine {
friend class GTextEditor;
friend class GTextDocument;
public:
explicit GTextDocumentLine(GTextEditor&);
GTextDocumentLine(GTextEditor&, const StringView&);
StringView view() const { return { characters(), length() }; }
const char* characters() const { return m_text.data(); }
int length() const { return m_text.size() - 1; }
void set_text(const StringView&);
void append(char);
void prepend(char);
void insert(int index, char);
void remove(int index);
void append(const char*, int);
void truncate(int length);
void clear();
void recompute_visual_lines();
int visual_line_containing(int column) const;
int first_non_whitespace_column() const;
template<typename Callback>
void for_each_visual_line(Callback) const;
private:
GTextEditor& m_editor;
// NOTE: This vector is null terminated.
Vector<char> m_text;
Vector<int, 1> m_visual_line_breaks;
Rect m_visual_rect;
};

View file

@ -19,6 +19,7 @@ GTextEditor::GTextEditor(Type type, GWidget* parent)
: GScrollableWidget(parent)
, m_type(type)
{
m_document = GTextDocument::create(*this);
set_frame_shape(FrameShape::Container);
set_frame_shadow(FrameShadow::Sunken);
set_frame_thickness(2);
@ -26,7 +27,6 @@ GTextEditor::GTextEditor(Type type, GWidget* parent)
set_font(GFontDatabase::the().get_by_name("Csilla Thin"));
// FIXME: Recompute vertical scrollbar step size on font change.
vertical_scrollbar().set_step(line_height());
m_lines.append(make<Line>(*this));
m_cursor = { 0, 0 };
create_actions();
}
@ -51,33 +51,17 @@ void GTextEditor::create_actions()
void GTextEditor::set_text(const StringView& text)
{
if (is_single_line() && text.length() == m_lines[0].length() && !memcmp(text.characters_without_null_termination(), m_lines[0].characters(), text.length()))
if (is_single_line() && text.length() == line(0).length() && !memcmp(text.characters_without_null_termination(), line(0).characters(), text.length()))
return;
m_spans.clear();
m_selection.clear();
m_lines.clear();
int start_of_current_line = 0;
auto add_line = [&](int current_position) {
int line_length = current_position - start_of_current_line;
auto line = make<Line>(*this);
if (line_length)
line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line));
m_lines.append(move(line));
start_of_current_line = current_position + 1;
};
int i = 0;
for (i = 0; i < text.length(); ++i) {
if (text[i] == '\n')
add_line(i);
}
add_line(i);
document().set_text(text);
update_content_size();
recompute_all_visual_lines();
if (is_single_line())
set_cursor(0, m_lines[0].length());
set_cursor(0, line(0).length());
else
set_cursor(0, 0);
did_update_selection();
@ -88,7 +72,7 @@ void GTextEditor::update_content_size()
{
int content_width = 0;
int content_height = 0;
for (auto& line : m_lines) {
for (auto& line : document().lines()) {
content_width = max(line.m_visual_rect.width(), content_width);
content_height += line.m_visual_rect.height();
}
@ -110,13 +94,13 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
int line_index = -1;
if (is_line_wrapping_enabled()) {
for (int i = 0; i < m_lines.size(); ++i) {
auto& rect = m_lines[i].m_visual_rect;
for (int i = 0; i < lines().size(); ++i) {
auto& rect = lines()[i].m_visual_rect;
if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
line_index = i;
break;
} else if (position.y() > rect.bottom())
line_index = m_lines.size() - 1;
line_index = lines().size() - 1;
}
} else {
line_index = position.y() / line_height();
@ -124,7 +108,7 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
line_index = max(0, min(line_index, line_count() - 1));
auto& line = m_lines[line_index];
auto& line = lines()[line_index];
int column_index;
switch (m_text_alignment) {
@ -149,7 +133,7 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
ASSERT_NOT_REACHED();
}
column_index = max(0, min(column_index, m_lines[line_index].length()));
column_index = max(0, min(column_index, lines()[line_index].length()));
return { line_index, column_index };
}
@ -163,9 +147,9 @@ void GTextEditor::doubleclick_event(GMouseEvent& event)
auto start = text_position_at(event.position());
auto end = start;
auto& line = m_lines[start.line()];
auto& line = lines()[start.line()];
if (m_spans.is_empty()) {
if (!document().has_spans()) {
while (start.column() > 0) {
if (isspace(line.characters()[start.column() - 1]))
break;
@ -178,7 +162,7 @@ void GTextEditor::doubleclick_event(GMouseEvent& event)
end.set_column(end.column() + 1);
}
} else {
for (auto& span : m_spans) {
for (auto& span : document().spans()) {
if (!span.range.contains(start))
continue;
start = span.range.start();
@ -209,11 +193,11 @@ void GTextEditor::mousedown_event(GMouseEvent& event)
if (is_multi_line()) {
// select *current* line
start = GTextPosition(m_cursor.line(), 0);
end = GTextPosition(m_cursor.line(), m_lines[m_cursor.line()].length());
end = GTextPosition(m_cursor.line(), lines()[m_cursor.line()].length());
} else {
// select *whole* line
start = GTextPosition(0, 0);
end = GTextPosition(line_count() - 1, m_lines[line_count() - 1].length());
end = GTextPosition(line_count() - 1, lines()[line_count() - 1].length());
}
m_selection.set(start, end);
@ -351,7 +335,7 @@ void GTextEditor::paint_event(GPaintEvent& event)
painter.add_clip_rect(text_clip_rect);
for (int line_index = first_visible_line; line_index <= last_visible_line; ++line_index) {
auto& line = m_lines[line_index];
auto& line = lines()[line_index];
bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
int first_visual_line_with_selection = -1;
@ -378,7 +362,7 @@ void GTextEditor::paint_event(GPaintEvent& event)
#ifdef DEBUG_GTEXTEDITOR
painter.draw_rect(visual_line_rect, Color::Cyan);
#endif
if (m_spans.is_empty()) {
if (!document().has_spans()) {
// Fast-path for plain text
painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, Color::Black);
} else {
@ -389,7 +373,7 @@ void GTextEditor::paint_event(GPaintEvent& event)
Color color;
GTextPosition physical_position(line_index, start_of_visual_line + i);
// FIXME: This is *horribly* inefficient.
for (auto& span : m_spans) {
for (auto& span : document().spans()) {
if (!span.range.contains(physical_position))
continue;
color = span.color;
@ -466,7 +450,7 @@ void GTextEditor::toggle_selection_if_needed_for_event(const GKeyEvent& event)
void GTextEditor::select_all()
{
GTextPosition start_of_document { 0, 0 };
GTextPosition end_of_document { line_count() - 1, m_lines[line_count() - 1].length() };
GTextPosition end_of_document { line_count() - 1, lines()[line_count() - 1].length() };
m_selection.set(start_of_document, end_of_document);
did_update_selection();
set_cursor(end_of_document);
@ -492,7 +476,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
if (event.key() == KeyCode::Key_Up) {
if (m_cursor.line() > 0) {
int new_line = m_cursor.line() - 1;
int new_column = min(m_cursor.column(), m_lines[new_line].length());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
@ -503,9 +487,9 @@ void GTextEditor::keydown_event(GKeyEvent& event)
return;
}
if (event.key() == KeyCode::Key_Down) {
if (m_cursor.line() < (m_lines.size() - 1)) {
if (m_cursor.line() < (lines().size() - 1)) {
int new_line = m_cursor.line() + 1;
int new_column = min(m_cursor.column(), m_lines[new_line].length());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
@ -518,7 +502,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
if (event.key() == KeyCode::Key_PageUp) {
if (m_cursor.line() > 0) {
int new_line = max(0, m_cursor.line() - visible_content_rect().height() / line_height());
int new_column = min(m_cursor.column(), m_lines[new_line].length());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
@ -529,9 +513,9 @@ void GTextEditor::keydown_event(GKeyEvent& event)
return;
}
if (event.key() == KeyCode::Key_PageDown) {
if (m_cursor.line() < (m_lines.size() - 1)) {
if (m_cursor.line() < (lines().size() - 1)) {
int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
int new_column = min(m_cursor.column(), m_lines[new_line].length());
int new_column = min(m_cursor.column(), lines()[new_line].length());
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
@ -552,7 +536,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
}
} else if (m_cursor.line() > 0) {
int new_line = m_cursor.line() - 1;
int new_column = m_lines[new_line].length();
int new_column = lines()[new_line].length();
toggle_selection_if_needed_for_event(event);
set_cursor(new_line, new_column);
if (event.shift() && m_selection.start().is_valid()) {
@ -613,7 +597,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
}
if (event.ctrl() && event.key() == KeyCode::Key_End) {
toggle_selection_if_needed_for_event(event);
set_cursor(line_count() - 1, m_lines[line_count() - 1].length());
set_cursor(line_count() - 1, lines()[line_count() - 1].length());
if (event.shift() && m_selection.start().is_valid()) {
m_selection.set_end(m_cursor);
did_update_selection();
@ -655,10 +639,10 @@ void GTextEditor::keydown_event(GKeyEvent& event)
}
if (m_cursor.column() == 0 && m_cursor.line() != 0) {
// Backspace at column 0; merge with previous line
auto& previous_line = m_lines[m_cursor.line() - 1];
auto& previous_line = lines()[m_cursor.line() - 1];
int previous_length = previous_line.length();
previous_line.append(current_line().characters(), current_line().length());
m_lines.remove(m_cursor.line());
lines().remove(m_cursor.line());
update_content_size();
update();
set_cursor(m_cursor.line() - 1, previous_length);
@ -691,9 +675,9 @@ void GTextEditor::delete_current_line()
if (has_selection())
return delete_selection();
m_lines.remove(m_cursor.line());
if (m_lines.is_empty())
m_lines.append(make<Line>(*this));
lines().remove(m_cursor.line());
if (lines().is_empty())
lines().append(make<GTextDocumentLine>(*this));
update_content_size();
update();
@ -716,10 +700,10 @@ void GTextEditor::do_delete()
}
if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) {
// Delete at end of line; merge with next line
auto& next_line = m_lines[m_cursor.line() + 1];
auto& next_line = lines()[m_cursor.line() + 1];
int previous_length = current_line().length();
current_line().append(next_line.characters(), next_line.length());
m_lines.remove(m_cursor.line() + 1);
lines().remove(m_cursor.line() + 1);
update();
did_change();
set_cursor(m_cursor.line(), previous_length);
@ -744,7 +728,7 @@ void GTextEditor::insert_at_cursor(char ch)
String new_line_contents;
if (m_automatic_indentation_enabled && at_tail) {
int leading_spaces = 0;
auto& old_line = m_lines[m_cursor.line()];
auto& old_line = lines()[m_cursor.line()];
for (int i = 0; i < old_line.length(); ++i) {
if (old_line.characters()[i] == ' ')
++leading_spaces;
@ -754,16 +738,16 @@ void GTextEditor::insert_at_cursor(char ch)
if (leading_spaces)
new_line_contents = String::repeated(' ', leading_spaces);
}
m_lines.insert(m_cursor.line() + (at_tail ? 1 : 0), make<Line>(*this, new_line_contents));
lines().insert(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(*this, new_line_contents));
update();
did_change();
set_cursor(m_cursor.line() + 1, m_lines[m_cursor.line() + 1].length());
set_cursor(m_cursor.line() + 1, lines()[m_cursor.line() + 1].length());
return;
}
auto new_line = make<Line>(*this);
auto new_line = make<GTextDocumentLine>(*this);
new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
current_line().truncate(m_cursor.column());
m_lines.insert(m_cursor.line() + 1, move(new_line));
lines().insert(m_cursor.line() + 1, move(new_line));
update();
did_change();
set_cursor(m_cursor.line() + 1, 0);
@ -786,7 +770,7 @@ void GTextEditor::insert_at_cursor(char ch)
int GTextEditor::content_x_for_position(const GTextPosition& position) const
{
auto& line = m_lines[position.line()];
auto& line = lines()[position.line()];
int x_offset = -1;
switch (m_text_alignment) {
case TextAlignment::CenterLeft:
@ -811,7 +795,7 @@ Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const
{
if (!position.is_valid())
return {};
ASSERT(!m_lines.is_empty());
ASSERT(!lines().is_empty());
ASSERT(position.column() <= (current_line().length() + 1));
int x = content_x_for_position(position);
@ -822,7 +806,7 @@ Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const
return rect;
}
auto& line = m_lines[position.line()];
auto& line = lines()[position.line()];
Rect rect;
line.for_each_visual_line([&](const Rect& visual_line_rect, const StringView& view, int start_of_visual_line) {
if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
@ -862,8 +846,8 @@ void GTextEditor::scroll_position_into_view(const GTextPosition& position)
auto rect = content_rect_for_position(position);
if (position.column() == 0)
rect.set_x(content_x_for_position({ position.line(), 0 }) - 2);
else if (position.column() == m_lines[position.line()].length())
rect.set_x(content_x_for_position({ position.line(), m_lines[position.line()].length() }) + 2);
else if (position.column() == lines()[position.line()].length())
rect.set_x(content_x_for_position({ position.line(), lines()[position.line()].length() }) + 2);
scroll_into_view(rect, true, true);
}
@ -874,7 +858,7 @@ void GTextEditor::scroll_cursor_into_view()
Rect GTextEditor::line_content_rect(int line_index) const
{
auto& line = m_lines[line_index];
auto& line = lines()[line_index];
if (is_single_line()) {
Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, line.length() * glyph_width(), font().glyph_height() + 2 };
line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
@ -902,19 +886,19 @@ void GTextEditor::set_cursor(int line, int column)
void GTextEditor::set_cursor(const GTextPosition& a_position)
{
ASSERT(!m_lines.is_empty());
ASSERT(!lines().is_empty());
GTextPosition position = a_position;
if (position.line() >= m_lines.size())
position.set_line(m_lines.size() - 1);
if (position.line() >= lines().size())
position.set_line(lines().size() - 1);
if (position.column() > m_lines[position.line()].length())
position.set_column(m_lines[position.line()].length());
if (position.column() > lines()[position.line()].length())
position.set_column(lines()[position.line()].length());
if (m_cursor != position) {
// NOTE: If the old cursor is no longer valid, repaint everything just in case.
auto old_cursor_line_rect = m_cursor.line() < m_lines.size()
auto old_cursor_line_rect = m_cursor.line() < lines().size()
? line_widget_rect(m_cursor.line())
: rect();
m_cursor = position;
@ -945,79 +929,6 @@ void GTextEditor::timer_event(CTimerEvent&)
update_cursor();
}
GTextEditor::Line::Line(GTextEditor& editor)
: m_editor(editor)
{
clear();
}
GTextEditor::Line::Line(GTextEditor& editor, const StringView& text)
: m_editor(editor)
{
set_text(text);
}
void GTextEditor::Line::clear()
{
m_text.clear();
m_text.append(0);
}
void GTextEditor::Line::set_text(const StringView& text)
{
if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length()))
return;
if (text.is_empty()) {
clear();
return;
}
m_text.resize(text.length() + 1);
memcpy(m_text.data(), text.characters_without_null_termination(), text.length() + 1);
}
void GTextEditor::Line::append(const char* characters, int length)
{
int old_length = m_text.size() - 1;
m_text.resize(m_text.size() + length);
memcpy(m_text.data() + old_length, characters, length);
m_text.last() = 0;
}
void GTextEditor::Line::append(char ch)
{
insert(length(), ch);
}
void GTextEditor::Line::prepend(char ch)
{
insert(0, ch);
}
void GTextEditor::Line::insert(int index, char ch)
{
if (index == length()) {
m_text.last() = ch;
m_text.append(0);
} else {
m_text.insert(index, move(ch));
}
}
void GTextEditor::Line::remove(int index)
{
if (index == length()) {
m_text.take_last();
m_text.last() = 0;
} else {
m_text.remove(index);
}
}
void GTextEditor::Line::truncate(int length)
{
m_text.resize(length + 1);
m_text.last() = 0;
}
bool GTextEditor::write_to_file(const StringView& path)
{
@ -1030,9 +941,9 @@ bool GTextEditor::write_to_file(const StringView& path)
// Compute the final file size and ftruncate() to make writing fast.
// FIXME: Remove this once the kernel is smart enough to do this instead.
off_t file_size = 0;
for (int i = 0; i < m_lines.size(); ++i)
file_size += m_lines[i].length();
file_size += m_lines.size() - 1;
for (int i = 0; i < lines().size(); ++i)
file_size += lines()[i].length();
file_size += lines().size() - 1;
int rc = ftruncate(fd, file_size);
if (rc < 0) {
@ -1040,8 +951,8 @@ bool GTextEditor::write_to_file(const StringView& path)
return false;
}
for (int i = 0; i < m_lines.size(); ++i) {
auto& line = m_lines[i];
for (int i = 0; i < lines().size(); ++i) {
auto& line = lines()[i];
if (line.length()) {
ssize_t nwritten = write(fd, line.characters(), line.length());
if (nwritten < 0) {
@ -1050,7 +961,7 @@ bool GTextEditor::write_to_file(const StringView& path)
return false;
}
}
if (i != m_lines.size() - 1) {
if (i != lines().size() - 1) {
char ch = '\n';
ssize_t nwritten = write(fd, &ch, 1);
if (nwritten != 1) {
@ -1069,7 +980,7 @@ String GTextEditor::text() const
{
StringBuilder builder;
for (int i = 0; i < line_count(); ++i) {
auto& line = m_lines[i];
auto& line = lines()[i];
builder.append(line.characters(), line.length());
if (i != line_count() - 1)
builder.append('\n');
@ -1079,8 +990,8 @@ String GTextEditor::text() const
void GTextEditor::clear()
{
m_lines.clear();
m_lines.append(make<Line>(*this));
lines().clear();
lines().append(make<GTextDocumentLine>(*this));
m_selection.clear();
did_update_selection();
set_cursor(0, 0);
@ -1095,7 +1006,7 @@ String GTextEditor::selected_text() const
auto selection = normalized_selection();
StringBuilder builder;
for (int i = selection.start().line(); i <= selection.end().line(); ++i) {
auto& line = m_lines[i];
auto& line = lines()[i];
int selection_start_column_on_line = selection.start().line() == i ? selection.start().column() : 0;
int selection_end_column_on_line = selection.end().line() == i ? selection.end().column() : line.length();
builder.append(line.characters() + selection_start_column_on_line, selection_end_column_on_line - selection_start_column_on_line);
@ -1115,13 +1026,13 @@ void GTextEditor::delete_selection()
// First delete all the lines in between the first and last one.
for (int i = selection.start().line() + 1; i < selection.end().line();) {
m_lines.remove(i);
lines().remove(i);
selection.end().set_line(selection.end().line() - 1);
}
if (selection.start().line() == selection.end().line()) {
// Delete within same line.
auto& line = m_lines[selection.start().line()];
auto& line = lines()[selection.start().line()];
bool whole_line_is_selected = selection.start().column() == 0 && selection.end().column() == line.length();
if (whole_line_is_selected) {
line.clear();
@ -1136,19 +1047,19 @@ void GTextEditor::delete_selection()
} else {
// Delete across a newline, merging lines.
ASSERT(selection.start().line() == selection.end().line() - 1);
auto& first_line = m_lines[selection.start().line()];
auto& second_line = m_lines[selection.end().line()];
auto& first_line = lines()[selection.start().line()];
auto& second_line = lines()[selection.end().line()];
auto before_selection = String(first_line.characters(), first_line.length()).substring(0, selection.start().column());
auto after_selection = String(second_line.characters(), second_line.length()).substring(selection.end().column(), second_line.length() - selection.end().column());
StringBuilder builder(before_selection.length() + after_selection.length());
builder.append(before_selection);
builder.append(after_selection);
first_line.set_text(builder.to_string());
m_lines.remove(selection.end().line());
lines().remove(selection.end().line());
}
if (m_lines.is_empty())
m_lines.append(make<Line>(*this));
if (lines().is_empty())
lines().append(make<GTextDocumentLine>(*this));
m_selection.clear();
did_update_selection();
@ -1278,7 +1189,7 @@ void GTextEditor::resize_event(GResizeEvent& event)
GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap)
{
auto& line = m_lines[position.line()];
auto& line = lines()[position.line()];
if (position.column() == line.length()) {
if (position.line() == line_count() - 1) {
if (should_wrap == ShouldWrapAtEndOfDocument::Yes)
@ -1295,12 +1206,12 @@ GTextPosition GTextEditor::prev_position_before(const GTextPosition& position, S
if (position.column() == 0) {
if (position.line() == 0) {
if (should_wrap == ShouldWrapAtStartOfDocument::Yes) {
auto& last_line = m_lines[line_count() - 1];
auto& last_line = lines()[line_count() - 1];
return { line_count() - 1, last_line.length() };
}
return {};
}
auto& prev_line = m_lines[position.line() - 1];
auto& prev_line = lines()[position.line() - 1];
return { position.line() - 1, prev_line.length() };
}
return { position.line(), position.column() - 1 };
@ -1380,7 +1291,7 @@ void GTextEditor::set_selection(const GTextRange& selection)
char GTextEditor::character_at(const GTextPosition& position) const
{
ASSERT(position.line() < line_count());
auto& line = m_lines[position.line()];
auto& line = lines()[position.line()];
if (position.column() == line.length())
return '\n';
return line.characters()[position.column()];
@ -1389,7 +1300,7 @@ char GTextEditor::character_at(const GTextPosition& position) const
void GTextEditor::recompute_all_visual_lines()
{
int y_offset = 0;
for (auto& line : m_lines) {
for (auto& line : lines()) {
line.recompute_visual_lines();
line.m_visual_rect.set_y(y_offset);
y_offset += line.m_visual_rect.height();
@ -1398,7 +1309,19 @@ void GTextEditor::recompute_all_visual_lines()
update_content_size();
}
void GTextEditor::Line::recompute_visual_lines()
int GTextDocumentLine::visual_line_containing(int column) const
{
int visual_line_index = 0;
for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) {
if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length()))
return IterationDecision::Break;
++visual_line_index;
return IterationDecision::Continue;
});
return visual_line_index;
}
void GTextDocumentLine::recompute_visual_lines()
{
m_visual_line_breaks.clear_with_capacity();
@ -1428,7 +1351,7 @@ void GTextEditor::Line::recompute_visual_lines()
}
template<typename Callback>
void GTextEditor::Line::for_each_visual_line(Callback callback) const
void GTextDocumentLine::for_each_visual_line(Callback callback) const
{
auto editor_visible_text_rect = m_editor.visible_text_rect_in_inner_coordinates();
int start_of_line = 0;
@ -1464,27 +1387,6 @@ void GTextEditor::set_line_wrapping_enabled(bool enabled)
update();
}
int GTextEditor::Line::visual_line_containing(int column) const
{
int visual_line_index = 0;
for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) {
if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length()))
return IterationDecision::Break;
++visual_line_index;
return IterationDecision::Continue;
});
return visual_line_index;
}
int GTextEditor::Line::first_non_whitespace_column() const
{
for (int i = 0; i < length(); ++i) {
if (!isspace(m_text[i]))
return i;
}
return length();
}
void GTextEditor::add_custom_context_menu_action(GAction& action)
{
m_custom_context_menu_actions.append(action);

View file

@ -6,6 +6,7 @@
#include <AK/NonnullRefPtrVector.h>
#include <LibDraw/TextAlignment.h>
#include <LibGUI/GScrollableWidget.h>
#include <LibGUI/GTextDocument.h>
#include <LibGUI/GTextRange.h>
class GAction;
@ -31,6 +32,9 @@ public:
};
virtual ~GTextEditor() override;
const GTextDocument& document() const { return *m_document; }
GTextDocument& document() { return *m_document; }
bool is_readonly() const { return m_readonly; }
void set_readonly(bool);
@ -56,7 +60,7 @@ public:
void set_text(const StringView&);
void scroll_cursor_into_view();
void scroll_position_into_view(const GTextPosition&);
int line_count() const { return m_lines.size(); }
int line_count() const { return document().line_count(); }
int line_spacing() const { return m_line_spacing; }
int line_height() const { return font().glyph_height() + m_line_spacing; }
GTextPosition cursor() const { return m_cursor; }
@ -103,17 +107,6 @@ public:
void set_cursor(int line, int column);
void set_cursor(const GTextPosition&);
struct Span {
GTextRange range;
Color color;
const Font* font { nullptr };
};
void set_spans(const Vector<Span>& spans)
{
m_spans = spans;
}
protected:
GTextEditor(Type, GWidget* parent);
@ -134,53 +127,24 @@ protected:
virtual void resize_event(GResizeEvent&) override;
private:
friend class GTextDocumentLine;
void create_actions();
void paint_ruler(Painter&);
void update_content_size();
void did_change();
class Line {
friend class GTextEditor;
public:
explicit Line(GTextEditor&);
Line(GTextEditor&, const StringView&);
StringView view() const { return { characters(), length() }; }
const char* characters() const { return m_text.data(); }
int length() const { return m_text.size() - 1; }
void set_text(const StringView&);
void append(char);
void prepend(char);
void insert(int index, char);
void remove(int index);
void append(const char*, int);
void truncate(int length);
void clear();
void recompute_visual_lines();
int visual_line_containing(int column) const;
int first_non_whitespace_column() const;
template<typename Callback>
void for_each_visual_line(Callback) const;
private:
GTextEditor& m_editor;
// NOTE: This vector is null terminated.
Vector<char> m_text;
Vector<int, 1> m_visual_line_breaks;
Rect m_visual_rect;
};
Rect line_content_rect(int item_index) const;
Rect line_widget_rect(int line_index) const;
Rect cursor_content_rect() const;
Rect content_rect_for_position(const GTextPosition&) const;
void update_cursor();
Line& current_line() { return m_lines[m_cursor.line()]; }
const Line& current_line() const { return m_lines[m_cursor.line()]; }
const NonnullOwnPtrVector<GTextDocumentLine>& lines() const { return document().lines(); }
NonnullOwnPtrVector<GTextDocumentLine>& lines() { return document().lines(); }
GTextDocumentLine& line(int index) { return document().line(index); }
const GTextDocumentLine& line(int index) const { return document().line(index); }
GTextDocumentLine& current_line() { return line(m_cursor.line()); }
const GTextDocumentLine& current_line() const { return line(m_cursor.line()); }
GTextPosition text_position_at(const Point&) const;
void insert_at_cursor(char);
void insert_at_cursor(const StringView&);
@ -198,7 +162,6 @@ private:
Type m_type { MultiLine };
NonnullOwnPtrVector<Line> m_lines;
GTextPosition m_cursor;
TextAlignment m_text_alignment { TextAlignment::CenterLeft };
bool m_cursor_state { true };
@ -222,7 +185,7 @@ private:
CElapsedTimer m_triple_click_timer;
NonnullRefPtrVector<GAction> m_custom_context_menu_actions;
Vector<Span> m_spans;
RefPtr<GTextDocument> m_document;
};
inline const LogStream& operator<<(const LogStream& stream, const GTextPosition& value)

View file

@ -25,6 +25,7 @@ OBJS = \
GVariant.o \
GShortcut.o \
GTextEditor.o \
GTextDocument.o \
GClipboard.o \
GSortingProxyModel.o \
GStackWidget.o \