LibGUI: Move visual line metadata from GTextDocument to GTextEditor

This patch decouples GTextDocument and GTextDocumentLine from the line
wrapping functionality of GTextEditor.

This should basically make it possible to have multiple GTextEditors
editing the same GTextDocument. Of course, that will require a bit more
work since there's no paint invalidation yet.
This commit is contained in:
Andreas Kling 2019-10-27 18:00:07 +01:00
parent f1c6193d6d
commit f96c683543
Notes: sideshowbarker 2024-07-19 11:31:10 +09:00
4 changed files with 187 additions and 82 deletions

View File

@ -1,24 +1,26 @@
#include <LibGUI/GTextDocument.h>
#include <ctype.h>
GTextDocument::GTextDocument(GTextEditor& editor)
: m_editor(editor)
GTextDocument::GTextDocument(Client* client)
{
m_lines.append(make<GTextDocumentLine>(m_editor));
if (client)
m_clients.set(client);
append_line(make<GTextDocumentLine>());
}
void GTextDocument::set_text(const StringView& text)
{
m_spans.clear();
m_lines.clear();
remove_all_lines();
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);
auto line = make<GTextDocumentLine>();
if (line_length)
line->set_text(text.substring_view(start_of_current_line, current_position - start_of_current_line));
m_lines.append(move(line));
append_line(move(line));
start_of_current_line = current_position + 1;
};
int i = 0;
@ -38,14 +40,12 @@ int GTextDocumentLine::first_non_whitespace_column() const
return length();
}
GTextDocumentLine::GTextDocumentLine(GTextEditor& editor)
: m_editor(editor)
GTextDocumentLine::GTextDocumentLine()
{
clear();
}
GTextDocumentLine::GTextDocumentLine(GTextEditor& editor, const StringView& text)
: m_editor(editor)
GTextDocumentLine::GTextDocumentLine(const StringView& text)
{
set_text(text);
}
@ -111,3 +111,45 @@ void GTextDocumentLine::truncate(int length)
m_text.resize(length + 1);
m_text.last() = 0;
}
void GTextDocument::append_line(NonnullOwnPtr<GTextDocumentLine> line)
{
lines().append(move(line));
for (auto* client : m_clients)
client->document_did_append_line();
}
void GTextDocument::insert_line(int line_index, NonnullOwnPtr<GTextDocumentLine> line)
{
lines().insert(line_index, move(line));
for (auto* client : m_clients)
client->document_did_insert_line(line_index);
}
void GTextDocument::remove_line(int line_index)
{
lines().remove(line_index);
for (auto* client : m_clients)
client->document_did_remove_line(line_index);
}
void GTextDocument::remove_all_lines()
{
lines().clear();
for (auto* client : m_clients)
client->document_did_remove_all_lines();
}
GTextDocument::Client::~Client()
{
}
void GTextDocument::register_client(Client& client)
{
m_clients.set(&client);
}
void GTextDocument::unregister_client(Client& client)
{
m_clients.remove(&client);
}

View File

@ -1,5 +1,6 @@
#pragma once
#include <AK/HashTable.h>
#include <AK/NonnullOwnPtrVector.h>
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
@ -18,9 +19,18 @@ struct GTextDocumentSpan {
class GTextDocument : public RefCounted<GTextDocument> {
public:
static NonnullRefPtr<GTextDocument> create(GTextEditor& editor)
class Client {
public:
virtual ~Client();
virtual void document_did_append_line() = 0;
virtual void document_did_insert_line(int) = 0;
virtual void document_did_remove_line(int) = 0;
virtual void document_did_remove_all_lines() = 0;
};
static NonnullRefPtr<GTextDocument> create(Client* client = nullptr)
{
return adopt(*new GTextDocument(editor));
return adopt(*new GTextDocument(client));
}
int line_count() const { return m_lines.size(); }
@ -37,13 +47,21 @@ public:
bool has_spans() const { return !m_spans.is_empty(); }
const Vector<GTextDocumentSpan>& spans() const { return m_spans; }
void append_line(NonnullOwnPtr<GTextDocumentLine>);
void remove_line(int line_index);
void remove_all_lines();
void insert_line(int line_index, NonnullOwnPtr<GTextDocumentLine>);
void register_client(Client&);
void unregister_client(Client&);
private:
explicit GTextDocument(GTextEditor&);
explicit GTextDocument(Client* client);
NonnullOwnPtrVector<GTextDocumentLine> m_lines;
Vector<GTextDocumentSpan> m_spans;
GTextEditor& m_editor;
HashTable<Client*> m_clients;
};
class GTextDocumentLine {
@ -51,8 +69,8 @@ class GTextDocumentLine {
friend class GTextDocument;
public:
explicit GTextDocumentLine(GTextEditor&);
GTextDocumentLine(GTextEditor&, const StringView&);
explicit GTextDocumentLine();
explicit GTextDocumentLine(const StringView&);
StringView view() const { return { characters(), length() }; }
const char* characters() const { return m_text.data(); }
@ -65,19 +83,9 @@ public:
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,7 +19,8 @@ GTextEditor::GTextEditor(Type type, GWidget* parent)
: GScrollableWidget(parent)
, m_type(type)
{
m_document = GTextDocument::create(*this);
m_document = GTextDocument::create(this);
m_document->register_client(*this);
set_frame_shape(FrameShape::Container);
set_frame_shadow(FrameShadow::Sunken);
set_frame_thickness(2);
@ -33,6 +34,8 @@ GTextEditor::GTextEditor(Type type, GWidget* parent)
GTextEditor::~GTextEditor()
{
if (m_document)
m_document->unregister_client(*this);
}
void GTextEditor::create_actions()
@ -72,9 +75,9 @@ void GTextEditor::update_content_size()
{
int content_width = 0;
int content_height = 0;
for (auto& line : document().lines()) {
content_width = max(line.m_visual_rect.width(), content_width);
content_height += line.m_visual_rect.height();
for (auto& line : m_line_visual_data) {
content_width = max(line.visual_rect.width(), content_width);
content_height += line.visual_rect.height();
}
content_width += m_horizontal_content_padding * 2;
if (is_right_text_alignment(m_text_alignment))
@ -95,11 +98,12 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
if (is_line_wrapping_enabled()) {
for (int i = 0; i < lines().size(); ++i) {
auto& rect = lines()[i].m_visual_rect;
auto& rect = m_line_visual_data[i].visual_rect;
if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
line_index = i;
break;
} else if (position.y() > rect.bottom())
}
if (position.y() > rect.bottom())
line_index = lines().size() - 1;
}
} else {
@ -108,14 +112,12 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
line_index = max(0, min(line_index, line_count() - 1));
auto& line = lines()[line_index];
int column_index;
switch (m_text_alignment) {
case TextAlignment::CenterLeft:
column_index = (position.x() + glyph_width() / 2) / glyph_width();
if (is_line_wrapping_enabled()) {
line.for_each_visual_line([&](const Rect& rect, const StringView&, int start_of_line) {
for_each_visual_line(line_index, [&](const Rect& rect, const StringView&, int start_of_line) {
if (rect.contains_vertically(position.y())) {
column_index += start_of_line;
return IterationDecision::Break;
@ -344,19 +346,19 @@ void GTextEditor::paint_event(GPaintEvent& event)
if (selection.start().line() < line_index)
first_visual_line_with_selection = 0;
else
first_visual_line_with_selection = line.visual_line_containing(selection.start().column());
first_visual_line_with_selection = visual_line_containing(line_index, selection.start().column());
if (selection.end().line() > line_index)
last_visual_line_with_selection = line.m_visual_line_breaks.size();
last_visual_line_with_selection = m_line_visual_data[line_index].visual_line_breaks.size();
else
last_visual_line_with_selection = line.visual_line_containing(selection.end().column());
last_visual_line_with_selection = visual_line_containing(line_index, selection.end().column());
}
int selection_start_column_within_line = selection.start().line() == line_index ? selection.start().column() : 0;
int selection_end_column_within_line = selection.end().line() == line_index ? selection.end().column() : line.length();
int visual_line_index = 0;
line.for_each_visual_line([&](const Rect& visual_line_rect, const StringView& visual_line_text, int start_of_visual_line) {
for_each_visual_line(line_index, [&](const Rect& visual_line_rect, const StringView& visual_line_text, int start_of_visual_line) {
if (is_multi_line() && line_index == m_cursor.line())
painter.fill_rect(visual_line_rect, Color(230, 230, 230));
#ifdef DEBUG_GTEXTEDITOR
@ -643,6 +645,7 @@ void GTextEditor::keydown_event(GKeyEvent& event)
int previous_length = previous_line.length();
previous_line.append(current_line().characters(), current_line().length());
lines().remove(m_cursor.line());
m_line_visual_data.remove(m_cursor.line());
update_content_size();
update();
set_cursor(m_cursor.line() - 1, previous_length);
@ -676,8 +679,9 @@ void GTextEditor::delete_current_line()
return delete_selection();
lines().remove(m_cursor.line());
m_line_visual_data.remove(m_cursor.line());
if (lines().is_empty())
lines().append(make<GTextDocumentLine>(*this));
document().append_line(make<GTextDocumentLine>());
update_content_size();
update();
@ -704,6 +708,7 @@ void GTextEditor::do_delete()
int previous_length = current_line().length();
current_line().append(next_line.characters(), next_line.length());
lines().remove(m_cursor.line() + 1);
m_line_visual_data.remove(m_cursor.line() + 1);
update();
did_change();
set_cursor(m_cursor.line(), previous_length);
@ -738,16 +743,16 @@ void GTextEditor::insert_at_cursor(char ch)
if (leading_spaces)
new_line_contents = String::repeated(' ', leading_spaces);
}
lines().insert(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(*this, new_line_contents));
document().insert_line(m_cursor.line() + (at_tail ? 1 : 0), make<GTextDocumentLine>(new_line_contents));
update();
did_change();
set_cursor(m_cursor.line() + 1, lines()[m_cursor.line() + 1].length());
return;
}
auto new_line = make<GTextDocumentLine>(*this);
auto new_line = make<GTextDocumentLine>();
new_line->append(current_line().characters() + m_cursor.column(), current_line().length() - m_cursor.column());
current_line().truncate(m_cursor.column());
lines().insert(m_cursor.line() + 1, move(new_line));
document().insert_line(m_cursor.line() + 1, move(new_line));
update();
did_change();
set_cursor(m_cursor.line() + 1, 0);
@ -774,7 +779,7 @@ int GTextEditor::content_x_for_position(const GTextPosition& position) const
int x_offset = -1;
switch (m_text_alignment) {
case TextAlignment::CenterLeft:
line.for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) {
for_each_visual_line(position.line(), [&](const Rect&, const StringView& view, int start_of_visual_line) {
if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
x_offset = (position.column() - start_of_visual_line) * glyph_width();
return IterationDecision::Break;
@ -806,9 +811,8 @@ Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const
return rect;
}
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) {
for_each_visual_line(position.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())) {
// NOTE: We have to subtract the horizontal padding here since it's part of the visual line rect
// *and* included in what we get from content_x_for_position().
@ -865,7 +869,7 @@ Rect GTextEditor::line_content_rect(int line_index) const
return line_rect;
}
if (is_line_wrapping_enabled())
return line.m_visual_rect;
return m_line_visual_data[line_index].visual_rect;
return {
content_x_for_position({ line_index, 0 }),
line_index * line_height(),
@ -929,7 +933,6 @@ void GTextEditor::timer_event(CTimerEvent&)
update_cursor();
}
bool GTextEditor::write_to_file(const StringView& path)
{
int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
@ -991,7 +994,8 @@ String GTextEditor::text() const
void GTextEditor::clear()
{
lines().clear();
lines().append(make<GTextDocumentLine>(*this));
m_line_visual_data.clear();
document().append_line(make<GTextDocumentLine>());
m_selection.clear();
did_update_selection();
set_cursor(0, 0);
@ -1027,6 +1031,7 @@ 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();) {
lines().remove(i);
m_line_visual_data.remove(i);
selection.end().set_line(selection.end().line() - 1);
}
@ -1056,10 +1061,12 @@ void GTextEditor::delete_selection()
builder.append(after_selection);
first_line.set_text(builder.to_string());
lines().remove(selection.end().line());
m_line_visual_data.remove(selection.end().line());
}
if (lines().is_empty())
lines().append(make<GTextDocumentLine>(*this));
if (lines().is_empty()) {
document().append_line(make<GTextDocumentLine>());
}
m_selection.clear();
did_update_selection();
@ -1300,19 +1307,19 @@ char GTextEditor::character_at(const GTextPosition& position) const
void GTextEditor::recompute_all_visual_lines()
{
int y_offset = 0;
for (auto& line : lines()) {
line.recompute_visual_lines();
line.m_visual_rect.set_y(y_offset);
y_offset += line.m_visual_rect.height();
for (int line_index = 0; line_index < line_count(); ++line_index) {
recompute_visual_lines(line_index);
m_line_visual_data[line_index].visual_rect.set_y(y_offset);
y_offset += m_line_visual_data[line_index].visual_rect.height();
}
update_content_size();
}
int GTextDocumentLine::visual_line_containing(int column) const
int GTextEditor::visual_line_containing(int line_index, int column) const
{
int visual_line_index = 0;
for_each_visual_line([&](const Rect&, const StringView& view, int start_of_visual_line) {
for_each_visual_line(line_index, [&](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;
@ -1321,20 +1328,23 @@ int GTextDocumentLine::visual_line_containing(int column) const
return visual_line_index;
}
void GTextDocumentLine::recompute_visual_lines()
void GTextEditor::recompute_visual_lines(int line_index)
{
m_visual_line_breaks.clear_with_capacity();
auto& line = document().line(line_index);
auto& visual_data = m_line_visual_data[line_index];
int available_width = m_editor.visible_text_rect_in_inner_coordinates().width();
visual_data.visual_line_breaks.clear_with_capacity();
if (m_editor.is_line_wrapping_enabled()) {
int available_width = visible_text_rect_in_inner_coordinates().width();
if (is_line_wrapping_enabled()) {
int line_width_so_far = 0;
for (int i = 0; i < length(); ++i) {
auto ch = characters()[i];
auto glyph_width = m_editor.font().glyph_width(ch);
for (int i = 0; i < line.length(); ++i) {
auto ch = line.characters()[i];
auto glyph_width = font().glyph_width(ch);
if ((line_width_so_far + glyph_width) > available_width) {
m_visual_line_breaks.append(i);
visual_data.visual_line_breaks.append(i);
line_width_so_far = glyph_width;
continue;
}
@ -1342,36 +1352,40 @@ void GTextDocumentLine::recompute_visual_lines()
}
}
m_visual_line_breaks.append(length());
visual_data.visual_line_breaks.append(line.length());
if (m_editor.is_line_wrapping_enabled())
m_visual_rect = { m_editor.m_horizontal_content_padding, 0, available_width, m_visual_line_breaks.size() * m_editor.line_height() };
if (is_line_wrapping_enabled())
visual_data.visual_rect = { m_horizontal_content_padding, 0, available_width, visual_data.visual_line_breaks.size() * line_height() };
else
m_visual_rect = { m_editor.m_horizontal_content_padding, 0, m_editor.font().width(view()), m_editor.line_height() };
visual_data.visual_rect = { m_horizontal_content_padding, 0, font().width(line.view()), line_height() };
}
template<typename Callback>
void GTextDocumentLine::for_each_visual_line(Callback callback) const
void GTextEditor::for_each_visual_line(int line_index, Callback callback) const
{
auto editor_visible_text_rect = m_editor.visible_text_rect_in_inner_coordinates();
auto editor_visible_text_rect = visible_text_rect_in_inner_coordinates();
int start_of_line = 0;
int line_index = 0;
for (auto visual_line_break : m_visual_line_breaks) {
auto visual_line_view = StringView(characters() + start_of_line, visual_line_break - start_of_line);
int visual_line_index = 0;
auto& line = document().line(line_index);
auto& visual_data = m_line_visual_data[line_index];
for (auto visual_line_break : visual_data.visual_line_breaks) {
auto visual_line_view = StringView(line.characters() + start_of_line, visual_line_break - start_of_line);
Rect visual_line_rect {
m_visual_rect.x(),
m_visual_rect.y() + (line_index * m_editor.line_height()),
m_editor.font().width(visual_line_view),
m_editor.line_height()
visual_data.visual_rect.x(),
visual_data.visual_rect.y() + (visual_line_index * line_height()),
font().width(visual_line_view),
line_height()
};
if (is_right_text_alignment(m_editor.text_alignment()))
if (is_right_text_alignment(text_alignment()))
visual_line_rect.set_right_without_resize(editor_visible_text_rect.right());
if (!m_editor.is_multi_line())
if (!is_multi_line())
visual_line_rect.center_vertically_within(editor_visible_text_rect);
if (callback(visual_line_rect, visual_line_view, start_of_line) == IterationDecision::Break)
break;
start_of_line = visual_line_break;
++line_index;
++visual_line_index;
}
}
@ -1397,3 +1411,24 @@ void GTextEditor::did_change_font()
vertical_scrollbar().set_step(line_height());
GWidget::did_change_font();
}
void GTextEditor::document_did_append_line()
{
m_line_visual_data.append(make<LineVisualData>());
}
void GTextEditor::document_did_remove_line(int line_index)
{
m_line_visual_data.remove(line_index);
}
void GTextEditor::document_did_remove_all_lines()
{
m_line_visual_data.clear();
}
void GTextEditor::document_did_insert_line(int line_index)
{
m_line_visual_data.insert(line_index, make<LineVisualData>());
}

View File

@ -23,7 +23,9 @@ enum class ShouldWrapAtStartOfDocument {
Yes
};
class GTextEditor : public GScrollableWidget {
class GTextEditor
: public GScrollableWidget
, public GTextDocument::Client {
C_OBJECT(GTextEditor)
public:
enum Type {
@ -129,6 +131,11 @@ protected:
private:
friend class GTextDocumentLine;
virtual void document_did_append_line() override;
virtual void document_did_insert_line(int) override;
virtual void document_did_remove_line(int) override;
virtual void document_did_remove_all_lines() override;
void create_actions();
void paint_ruler(Painter&);
void update_content_size();
@ -160,6 +167,9 @@ private:
Rect visible_text_rect_in_inner_coordinates() const;
void recompute_all_visual_lines();
int visual_line_containing(int line_index, int column) const;
void recompute_visual_lines(int line_index);
Type m_type { MultiLine };
GTextPosition m_cursor;
@ -186,6 +196,16 @@ private:
NonnullRefPtrVector<GAction> m_custom_context_menu_actions;
RefPtr<GTextDocument> m_document;
template<typename Callback>
void for_each_visual_line(int line_index, Callback) const;
struct LineVisualData {
Vector<int, 1> visual_line_breaks;
Rect visual_rect;
};
NonnullOwnPtrVector<LineVisualData> m_line_visual_data;
};
inline const LogStream& operator<<(const LogStream& stream, const GTextPosition& value)