mirror of
https://github.com/RGBCube/serenity
synced 2025-05-30 21:58:10 +00:00
GTextEditor: Start working on a line-wrapping feature
This is not finished, but since the feature is controlled by a runtime flag, the broken implementation should not affect users of this widget too much (in theory :^).)
This commit is contained in:
parent
280a9a2f34
commit
9752e683f6
2 changed files with 126 additions and 20 deletions
|
@ -96,6 +96,7 @@ void GTextEditor::set_text(const StringView& text)
|
||||||
}
|
}
|
||||||
add_line(i);
|
add_line(i);
|
||||||
update_content_size();
|
update_content_size();
|
||||||
|
recompute_all_visual_lines();
|
||||||
if (is_single_line())
|
if (is_single_line())
|
||||||
set_cursor(0, m_lines[0].length());
|
set_cursor(0, m_lines[0].length());
|
||||||
else
|
else
|
||||||
|
@ -124,15 +125,41 @@ GTextPosition GTextEditor::text_position_at(const Point& a_position) const
|
||||||
position.move_by(-(m_horizontal_content_padding + ruler_width()), 0);
|
position.move_by(-(m_horizontal_content_padding + ruler_width()), 0);
|
||||||
position.move_by(-frame_thickness(), -frame_thickness());
|
position.move_by(-frame_thickness(), -frame_thickness());
|
||||||
|
|
||||||
int line_index = position.y() / line_height();
|
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;
|
||||||
|
if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
|
||||||
|
line_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line_index = position.y() / line_height();
|
||||||
|
}
|
||||||
|
|
||||||
line_index = max(0, min(line_index, line_count() - 1));
|
line_index = max(0, min(line_index, line_count() - 1));
|
||||||
|
|
||||||
|
auto& line = m_lines[line_index];
|
||||||
|
|
||||||
int column_index;
|
int column_index;
|
||||||
switch (m_text_alignment) {
|
switch (m_text_alignment) {
|
||||||
case TextAlignment::CenterLeft:
|
case TextAlignment::CenterLeft:
|
||||||
column_index = (position.x() + glyph_width() / 2) / glyph_width();
|
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) {
|
||||||
|
if (rect.contains(position)) {
|
||||||
|
column_index += start_of_line;
|
||||||
|
return IterationDecision::Break;
|
||||||
|
}
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case TextAlignment::CenterRight:
|
case TextAlignment::CenterRight:
|
||||||
|
// FIXME: Support right-aligned line wrapping, I guess.
|
||||||
|
ASSERT(!is_line_wrapping_enabled());
|
||||||
column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width();
|
column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -330,24 +357,26 @@ void GTextEditor::paint_event(GPaintEvent& event)
|
||||||
|
|
||||||
for (int i = first_visible_line; i <= last_visible_line; ++i) {
|
for (int i = first_visible_line; i <= last_visible_line; ++i) {
|
||||||
auto& line = m_lines[i];
|
auto& line = m_lines[i];
|
||||||
auto line_rect = line_content_rect(i);
|
line.for_each_visual_line([&](const Rect& line_rect, const StringView& visual_line_text, int) {
|
||||||
// FIXME: Make sure we always fill the entire line.
|
// FIXME: Make sure we always fill the entire line.
|
||||||
//line_rect.set_width(exposed_width);
|
//line_rect.set_width(exposed_width);
|
||||||
if (is_multi_line() && i == m_cursor.line())
|
if (is_multi_line() && i == m_cursor.line())
|
||||||
painter.fill_rect(line_rect, Color(230, 230, 230));
|
painter.fill_rect(line_rect, Color(230, 230, 230));
|
||||||
painter.draw_text(line_rect, StringView(line.characters(), line.length()), m_text_alignment, Color::Black);
|
painter.draw_text(line_rect, visual_line_text, m_text_alignment, Color::Black);
|
||||||
bool line_has_selection = has_selection && i >= selection.start().line() && i <= selection.end().line();
|
bool line_has_selection = has_selection && i >= selection.start().line() && i <= selection.end().line();
|
||||||
if (line_has_selection) {
|
if (line_has_selection) {
|
||||||
int selection_start_column_on_line = selection.start().line() == i ? selection.start().column() : 0;
|
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();
|
int selection_end_column_on_line = selection.end().line() == i ? selection.end().column() : line.length();
|
||||||
|
|
||||||
int selection_left = content_x_for_position({ i, selection_start_column_on_line });
|
int selection_left = content_x_for_position({ i, selection_start_column_on_line });
|
||||||
int selection_right = content_x_for_position({ i, selection_end_column_on_line });
|
int selection_right = content_x_for_position({ i, selection_end_column_on_line });
|
||||||
|
|
||||||
Rect selection_rect { selection_left, line_rect.y(), selection_right - selection_left, line_rect.height() };
|
Rect selection_rect { selection_left, line_rect.y(), selection_right - selection_left, line_rect.height() };
|
||||||
painter.fill_rect(selection_rect, Color::from_rgb(0x955233));
|
painter.fill_rect(selection_rect, Color::from_rgb(0x955233));
|
||||||
painter.draw_text(selection_rect, StringView(line.characters() + selection_start_column_on_line, line.length() - selection_start_column_on_line - (line.length() - selection_end_column_on_line)), TextAlignment::CenterLeft, Color::White);
|
painter.draw_text(selection_rect, StringView(line.characters() + selection_start_column_on_line, line.length() - selection_start_column_on_line - (line.length() - selection_end_column_on_line)), TextAlignment::CenterLeft, Color::White);
|
||||||
}
|
}
|
||||||
|
return IterationDecision::Continue;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_focused() && m_cursor_state)
|
if (is_focused() && m_cursor_state)
|
||||||
|
@ -745,6 +774,8 @@ Rect GTextEditor::line_content_rect(int line_index) const
|
||||||
line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
|
line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
|
||||||
return line_rect;
|
return line_rect;
|
||||||
}
|
}
|
||||||
|
if (is_line_wrapping_enabled())
|
||||||
|
return line.m_visual_rect;
|
||||||
return {
|
return {
|
||||||
content_x_for_position({ line_index, 0 }),
|
content_x_for_position({ line_index, 0 }),
|
||||||
line_index * line_height(),
|
line_index * line_height(),
|
||||||
|
@ -833,7 +864,9 @@ void GTextEditor::Line::set_text(const StringView& text)
|
||||||
|
|
||||||
int GTextEditor::Line::width(const Font& font) const
|
int GTextEditor::Line::width(const Font& font) const
|
||||||
{
|
{
|
||||||
return font.glyph_width('x') * length();
|
if (m_editor.is_line_wrapping_enabled())
|
||||||
|
return m_editor.visible_text_rect_in_inner_coordinates().width();
|
||||||
|
return font.width(view());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GTextEditor::Line::append(const char* characters, int length)
|
void GTextEditor::Line::append(const char* characters, int length)
|
||||||
|
@ -1054,6 +1087,7 @@ void GTextEditor::did_change()
|
||||||
{
|
{
|
||||||
ASSERT(!is_readonly());
|
ASSERT(!is_readonly());
|
||||||
update_content_size();
|
update_content_size();
|
||||||
|
recompute_all_visual_lines();
|
||||||
if (!m_have_pending_change_notification) {
|
if (!m_have_pending_change_notification) {
|
||||||
m_have_pending_change_notification = true;
|
m_have_pending_change_notification = true;
|
||||||
deferred_invoke([this](auto&) {
|
deferred_invoke([this](auto&) {
|
||||||
|
@ -1109,6 +1143,7 @@ void GTextEditor::resize_event(GResizeEvent& event)
|
||||||
{
|
{
|
||||||
GScrollableWidget::resize_event(event);
|
GScrollableWidget::resize_event(event);
|
||||||
update_content_size();
|
update_content_size();
|
||||||
|
recompute_all_visual_lines();
|
||||||
}
|
}
|
||||||
|
|
||||||
GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap)
|
GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap)
|
||||||
|
@ -1219,3 +1254,62 @@ char GTextEditor::character_at(const GTextPosition& position) const
|
||||||
return '\n';
|
return '\n';
|
||||||
return line.characters()[position.column()];
|
return line.characters()[position.column()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GTextEditor::recompute_all_visual_lines()
|
||||||
|
{
|
||||||
|
int y_offset = 0;
|
||||||
|
for (auto& line : m_lines) {
|
||||||
|
line.recompute_visual_lines();
|
||||||
|
line.m_visual_rect.set_y(y_offset);
|
||||||
|
y_offset += line.m_visual_rect.height();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GTextEditor::Line::recompute_visual_lines()
|
||||||
|
{
|
||||||
|
m_visual_line_breaks.clear_with_capacity();
|
||||||
|
|
||||||
|
int available_width = m_editor.visible_text_rect_in_inner_coordinates().width();
|
||||||
|
|
||||||
|
if (m_editor.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);
|
||||||
|
if ((line_width_so_far + glyph_width) > available_width) {
|
||||||
|
m_visual_line_breaks.append(i);
|
||||||
|
line_width_so_far = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
line_width_so_far += glyph_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_visual_line_breaks.append(length());
|
||||||
|
|
||||||
|
if (m_editor.is_line_wrapping_enabled())
|
||||||
|
m_visual_rect = { 0, 0, available_width, m_visual_line_breaks.size() * m_editor.line_height() };
|
||||||
|
else
|
||||||
|
m_visual_rect = { 0, 0, m_editor.font().width(view()), m_editor.line_height() };
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Callback>
|
||||||
|
void GTextEditor::Line::for_each_visual_line(Callback callback) const
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
Rect visual_line_rect {
|
||||||
|
m_visual_rect.x(),
|
||||||
|
m_visual_rect.y() + (line_index * m_editor.line_height()),
|
||||||
|
m_visual_rect.width(),
|
||||||
|
m_editor.line_height()
|
||||||
|
};
|
||||||
|
if (callback(visual_line_rect, visual_line_view, start_of_line) == IterationDecision::Break)
|
||||||
|
break;
|
||||||
|
start_of_line = visual_line_break;
|
||||||
|
++line_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -101,6 +101,9 @@ public:
|
||||||
bool is_automatic_indentation_enabled() const { return m_automatic_indentation_enabled; }
|
bool is_automatic_indentation_enabled() const { return m_automatic_indentation_enabled; }
|
||||||
void set_automatic_indentation_enabled(bool enabled) { m_automatic_indentation_enabled = enabled; }
|
void set_automatic_indentation_enabled(bool enabled) { m_automatic_indentation_enabled = enabled; }
|
||||||
|
|
||||||
|
bool is_line_wrapping_enabled() const { return m_line_wrapping_enabled; }
|
||||||
|
void set_line_wrapping_enabled(bool enabled) { m_line_wrapping_enabled = enabled; }
|
||||||
|
|
||||||
TextAlignment text_alignment() const { return m_text_alignment; }
|
TextAlignment text_alignment() const { return m_text_alignment; }
|
||||||
void set_text_alignment(TextAlignment);
|
void set_text_alignment(TextAlignment);
|
||||||
|
|
||||||
|
@ -132,7 +135,7 @@ public:
|
||||||
|
|
||||||
GTextPosition next_position_after(const GTextPosition&, ShouldWrapAtEndOfDocument = ShouldWrapAtEndOfDocument::Yes);
|
GTextPosition next_position_after(const GTextPosition&, ShouldWrapAtEndOfDocument = ShouldWrapAtEndOfDocument::Yes);
|
||||||
GTextPosition prev_position_before(const GTextPosition&, ShouldWrapAtStartOfDocument = ShouldWrapAtStartOfDocument::Yes);
|
GTextPosition prev_position_before(const GTextPosition&, ShouldWrapAtStartOfDocument = ShouldWrapAtStartOfDocument::Yes);
|
||||||
|
|
||||||
bool has_selection() const { return m_selection.is_valid(); }
|
bool has_selection() const { return m_selection.is_valid(); }
|
||||||
String selected_text() const;
|
String selected_text() const;
|
||||||
void set_selection(const GTextRange&);
|
void set_selection(const GTextRange&);
|
||||||
|
@ -187,6 +190,7 @@ private:
|
||||||
explicit Line(GTextEditor&);
|
explicit Line(GTextEditor&);
|
||||||
Line(GTextEditor&, const StringView&);
|
Line(GTextEditor&, const StringView&);
|
||||||
|
|
||||||
|
StringView view() const { return { characters(), length() }; }
|
||||||
const char* characters() const { return m_text.data(); }
|
const char* characters() const { return m_text.data(); }
|
||||||
int length() const { return m_text.size() - 1; }
|
int length() const { return m_text.size() - 1; }
|
||||||
int width(const Font&) const;
|
int width(const Font&) const;
|
||||||
|
@ -198,12 +202,19 @@ private:
|
||||||
void append(const char*, int);
|
void append(const char*, int);
|
||||||
void truncate(int length);
|
void truncate(int length);
|
||||||
void clear();
|
void clear();
|
||||||
|
void recompute_visual_lines();
|
||||||
|
|
||||||
|
template<typename Callback>
|
||||||
|
void for_each_visual_line(Callback) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
GTextEditor& m_editor;
|
GTextEditor& m_editor;
|
||||||
|
|
||||||
// NOTE: This vector is null terminated.
|
// NOTE: This vector is null terminated.
|
||||||
Vector<char> m_text;
|
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_content_rect(int item_index) const;
|
||||||
|
@ -228,6 +239,7 @@ private:
|
||||||
char character_at(const GTextPosition&) const;
|
char character_at(const GTextPosition&) const;
|
||||||
Rect ruler_rect_in_inner_coordinates() const;
|
Rect ruler_rect_in_inner_coordinates() const;
|
||||||
Rect visible_text_rect_in_inner_coordinates() const;
|
Rect visible_text_rect_in_inner_coordinates() const;
|
||||||
|
void recompute_all_visual_lines();
|
||||||
|
|
||||||
Type m_type { MultiLine };
|
Type m_type { MultiLine };
|
||||||
|
|
||||||
|
@ -239,6 +251,7 @@ private:
|
||||||
bool m_ruler_visible { false };
|
bool m_ruler_visible { false };
|
||||||
bool m_have_pending_change_notification { false };
|
bool m_have_pending_change_notification { false };
|
||||||
bool m_automatic_indentation_enabled { false };
|
bool m_automatic_indentation_enabled { false };
|
||||||
|
bool m_line_wrapping_enabled { false };
|
||||||
bool m_readonly { false };
|
bool m_readonly { false };
|
||||||
int m_line_spacing { 4 };
|
int m_line_spacing { 4 };
|
||||||
int m_soft_tab_width { 4 };
|
int m_soft_tab_width { 4 };
|
||||||
|
@ -267,4 +280,3 @@ inline const LogStream& operator<<(const LogStream& stream, const GTextRange& va
|
||||||
return stream << "GTextRange(Invalid)";
|
return stream << "GTextRange(Invalid)";
|
||||||
return stream << value.start() << '-' << value.end();
|
return stream << value.start() << '-' << value.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue