summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DevTools/HackStudio/main.cpp6
-rw-r--r--Libraries/LibGUI/GTextDocument.cpp113
-rw-r--r--Libraries/LibGUI/GTextDocument.h83
-rw-r--r--Libraries/LibGUI/GTextEditor.cpp276
-rw-r--r--Libraries/LibGUI/GTextEditor.h65
-rw-r--r--Libraries/LibGUI/Makefile1
6 files changed, 303 insertions, 241 deletions
diff --git a/DevTools/HackStudio/main.cpp b/DevTools/HackStudio/main.cpp
index dd306fa228..9ae7ef47c4 100644
--- a/DevTools/HackStudio/main.cpp
+++ b/DevTools/HackStudio/main.cpp
@@ -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();
}
diff --git a/Libraries/LibGUI/GTextDocument.cpp b/Libraries/LibGUI/GTextDocument.cpp
new file mode 100644
index 0000000000..449a7a2142
--- /dev/null
+++ b/Libraries/LibGUI/GTextDocument.cpp
@@ -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;
+}
diff --git a/Libraries/LibGUI/GTextDocument.h b/Libraries/LibGUI/GTextDocument.h
new file mode 100644
index 0000000000..b1bd94246d
--- /dev/null
+++ b/Libraries/LibGUI/GTextDocument.h
@@ -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;
+};
diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp
index 40efa48388..abb5b911c2 100644
--- a/Libraries/LibGUI/GTextEditor.cpp
+++ b/Libraries/LibGUI/GTextEditor.cpp
@@ -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);
diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h
index 1e9b4649f8..a56e343fda 100644
--- a/Libraries/LibGUI/GTextEditor.h
+++ b/Libraries/LibGUI/GTextEditor.h
@@ -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)
diff --git a/Libraries/LibGUI/Makefile b/Libraries/LibGUI/Makefile
index 24d3d70ad7..28921c6c7f 100644
--- a/Libraries/LibGUI/Makefile
+++ b/Libraries/LibGUI/Makefile
@@ -25,6 +25,7 @@ OBJS = \
GVariant.o \
GShortcut.o \
GTextEditor.o \
+ GTextDocument.o \
GClipboard.o \
GSortingProxyModel.o \
GStackWidget.o \