diff options
author | Andreas Kling <awesomekling@gmail.com> | 2019-08-25 08:43:01 +0200 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2019-08-25 11:24:23 +0200 |
commit | 9752e683f676cfd1ab1bac9125c09fb84d0c8efa (patch) | |
tree | cc4ad0d7718b6c2458d682cf4f7380442b8d2168 /Libraries | |
parent | 280a9a2f34c10224533ce81f824a2bbe1b94f1fc (diff) | |
download | serenity-9752e683f676cfd1ab1bac9125c09fb84d0c8efa.zip |
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 :^).)
Diffstat (limited to 'Libraries')
-rw-r--r-- | Libraries/LibGUI/GTextEditor.cpp | 134 | ||||
-rw-r--r-- | Libraries/LibGUI/GTextEditor.h | 16 |
2 files changed, 128 insertions, 22 deletions
diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp index d475b49852..19b90522a6 100644 --- a/Libraries/LibGUI/GTextEditor.cpp +++ b/Libraries/LibGUI/GTextEditor.cpp @@ -96,6 +96,7 @@ void GTextEditor::set_text(const StringView& text) } add_line(i); update_content_size(); + recompute_all_visual_lines(); if (is_single_line()) set_cursor(0, m_lines[0].length()); 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(-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)); + auto& line = m_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) { + if (rect.contains(position)) { + column_index += start_of_line; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + } break; 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(); break; default: @@ -330,24 +357,26 @@ void GTextEditor::paint_event(GPaintEvent& event) for (int i = first_visible_line; i <= last_visible_line; ++i) { auto& line = m_lines[i]; - auto line_rect = line_content_rect(i); - // FIXME: Make sure we always fill the entire line. - //line_rect.set_width(exposed_width); - if (is_multi_line() && i == m_cursor.line()) - painter.fill_rect(line_rect, Color(230, 230, 230)); - painter.draw_text(line_rect, StringView(line.characters(), line.length()), m_text_alignment, Color::Black); - bool line_has_selection = has_selection && i >= selection.start().line() && i <= selection.end().line(); - if (line_has_selection) { - 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_left = content_x_for_position({ i, selection_start_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() }; - 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); - } + line.for_each_visual_line([&](const Rect& line_rect, const StringView& visual_line_text, int) { + // FIXME: Make sure we always fill the entire line. + //line_rect.set_width(exposed_width); + if (is_multi_line() && i == m_cursor.line()) + painter.fill_rect(line_rect, Color(230, 230, 230)); + 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(); + if (line_has_selection) { + 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_left = content_x_for_position({ i, selection_start_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() }; + 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); + } + return IterationDecision::Continue; + }); } 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() }); return line_rect; } + if (is_line_wrapping_enabled()) + return line.m_visual_rect; return { content_x_for_position({ line_index, 0 }), line_index * line_height(), @@ -833,7 +864,9 @@ void GTextEditor::Line::set_text(const StringView& text) 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) @@ -1054,6 +1087,7 @@ void GTextEditor::did_change() { ASSERT(!is_readonly()); update_content_size(); + recompute_all_visual_lines(); if (!m_have_pending_change_notification) { m_have_pending_change_notification = true; deferred_invoke([this](auto&) { @@ -1109,6 +1143,7 @@ void GTextEditor::resize_event(GResizeEvent& event) { GScrollableWidget::resize_event(event); update_content_size(); + recompute_all_visual_lines(); } 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 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; + } +} diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h index 8a98d33be1..104aa82d90 100644 --- a/Libraries/LibGUI/GTextEditor.h +++ b/Libraries/LibGUI/GTextEditor.h @@ -101,6 +101,9 @@ public: bool is_automatic_indentation_enabled() const { return m_automatic_indentation_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; } void set_text_alignment(TextAlignment); @@ -132,7 +135,7 @@ public: GTextPosition next_position_after(const GTextPosition&, ShouldWrapAtEndOfDocument = ShouldWrapAtEndOfDocument::Yes); GTextPosition prev_position_before(const GTextPosition&, ShouldWrapAtStartOfDocument = ShouldWrapAtStartOfDocument::Yes); - + bool has_selection() const { return m_selection.is_valid(); } String selected_text() const; void set_selection(const GTextRange&); @@ -187,6 +190,7 @@ private: 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; } int width(const Font&) const; @@ -198,12 +202,19 @@ private: void append(const char*, int); void truncate(int length); void clear(); + void recompute_visual_lines(); + + 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; @@ -228,6 +239,7 @@ private: char character_at(const GTextPosition&) const; Rect ruler_rect_in_inner_coordinates() const; Rect visible_text_rect_in_inner_coordinates() const; + void recompute_all_visual_lines(); Type m_type { MultiLine }; @@ -239,6 +251,7 @@ private: bool m_ruler_visible { false }; bool m_have_pending_change_notification { false }; bool m_automatic_indentation_enabled { false }; + bool m_line_wrapping_enabled { false }; bool m_readonly { false }; int m_line_spacing { 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 << value.start() << '-' << value.end(); } - |