diff options
author | Andreas Kling <awesomekling@gmail.com> | 2019-08-21 21:23:17 +0200 |
---|---|---|
committer | Andreas Kling <awesomekling@gmail.com> | 2019-08-21 21:23:17 +0200 |
commit | 0c72371ad9ebbd5d619a854eca7b8fd3333b95d1 (patch) | |
tree | 6accd824aef6d06799573c7197283575a7f2bcad /Libraries/LibGUI | |
parent | 5670a3e0649ba7cf6459a1099260ea2e5eced1b6 (diff) | |
download | serenity-0c72371ad9ebbd5d619a854eca7b8fd3333b95d1.zip |
GTextEditor: Implement a simple text search API
- GTextRange find(const StringView& needle, const GTextPosition& start)
This function searches for the needle in the haystack (the full text)
and returns a GTextRange for the closest match after "start".
If the needle is not found, it returns an invalid GTextRange.
If no "start" position is provided, the search begins at the head of
the text document. :^)
Diffstat (limited to 'Libraries/LibGUI')
-rw-r--r-- | Libraries/LibGUI/GTextEditor.cpp | 100 | ||||
-rw-r--r-- | Libraries/LibGUI/GTextEditor.h | 30 |
2 files changed, 116 insertions, 14 deletions
diff --git a/Libraries/LibGUI/GTextEditor.cpp b/Libraries/LibGUI/GTextEditor.cpp index e43590a186..841cef6493 100644 --- a/Libraries/LibGUI/GTextEditor.cpp +++ b/Libraries/LibGUI/GTextEditor.cpp @@ -668,21 +668,27 @@ int GTextEditor::content_x_for_position(const GTextPosition& position) const } } -Rect GTextEditor::cursor_content_rect() const +Rect GTextEditor::content_rect_for_position(const GTextPosition& position) const { - if (!m_cursor.is_valid()) + if (!position.is_valid()) return {}; ASSERT(!m_lines.is_empty()); - ASSERT(m_cursor.column() <= (current_line().length() + 1)); + ASSERT(position.column() <= (current_line().length() + 1)); - int cursor_x = content_x_for_position(m_cursor); + int x = content_x_for_position(position); if (is_single_line()) { - Rect cursor_rect { cursor_x, 0, 1, font().glyph_height() + 2 }; - cursor_rect.center_vertically_within({ {}, frame_inner_rect().size() }); - return cursor_rect; + Rect rect { x, 0, 1, font().glyph_height() + 2 }; + rect.center_vertically_within({ {}, frame_inner_rect().size() }); + return rect; } - return { cursor_x, m_cursor.line() * line_height(), 1, line_height() }; + return { x, position.line() * line_height(), 1, line_height() }; +} + + +Rect GTextEditor::cursor_content_rect() const +{ + return content_rect_for_position(m_cursor); } Rect GTextEditor::line_widget_rect(int line_index) const @@ -696,16 +702,21 @@ Rect GTextEditor::line_widget_rect(int line_index) const return rect; } -void GTextEditor::scroll_cursor_into_view() +void GTextEditor::scroll_position_into_view(const GTextPosition& position) { - auto rect = cursor_content_rect(); - if (m_cursor.column() == 0) - rect.set_x(content_x_for_position({ m_cursor.line(), 0 }) - 2); - else if (m_cursor.column() == m_lines[m_cursor.line()].length()) - rect.set_x(content_x_for_position({ m_cursor.line(), m_lines[m_cursor.line()].length() }) + 2); + 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); scroll_into_view(rect, true, true); } +void GTextEditor::scroll_cursor_into_view() +{ + scroll_position_into_view(m_cursor); +} + Rect GTextEditor::line_content_rect(int line_index) const { auto& line = m_lines[line_index]; @@ -1079,3 +1090,64 @@ void GTextEditor::resize_event(GResizeEvent& event) GScrollableWidget::resize_event(event); update_content_size(); } + +GTextPosition GTextEditor::next_position_after(const GTextPosition& position, ShouldWrapAtEndOfDocument should_wrap) +{ + auto& line = m_lines[position.line()]; + if (position.column() == line.length()) { + if (position.line() == line_count() - 1) { + if (should_wrap == ShouldWrapAtEndOfDocument::Yes) + return { 0, 0 }; + return {}; + } + return { position.line() + 1, 0 }; + } + return { position.line(), position.column() + 1 }; +} + +GTextRange GTextEditor::find(const StringView& needle, const GTextPosition& start) +{ + if (needle.is_empty()) + return {}; + + GTextPosition position = start.is_valid() ? start : GTextPosition(0, 0); + GTextPosition original_position = position; + + GTextPosition start_of_potential_match; + int needle_index = 0; + + do { + auto ch = character_at(position); + if (ch == needle[needle_index]) { + if (needle_index == 0) + start_of_potential_match = position; + ++needle_index; + if (needle_index >= needle.length()) + return { start_of_potential_match, next_position_after(position) }; + } else { + needle_index = 0; + } + position = next_position_after(position); + } while(position.is_valid() && position != original_position); + + return {}; +} + +void GTextEditor::set_selection(const GTextRange& selection) +{ + if (m_selection == selection) + return; + m_selection = selection; + set_cursor(m_selection.end()); + scroll_position_into_view(normalized_selection().start()); + update(); +} + +char GTextEditor::character_at(const GTextPosition& position) const +{ + ASSERT(position.line() < line_count()); + auto& line = m_lines[position.line()]; + if (position.column() == line.length()) + return '\n'; + return line.characters()[position.column()]; +} diff --git a/Libraries/LibGUI/GTextEditor.h b/Libraries/LibGUI/GTextEditor.h index 2b858e8d05..be9468644b 100644 --- a/Libraries/LibGUI/GTextEditor.h +++ b/Libraries/LibGUI/GTextEditor.h @@ -11,6 +11,8 @@ class GMenu; class GScrollBar; class Painter; +enum class ShouldWrapAtEndOfDocument { No = 0, Yes }; + class GTextPosition { public: GTextPosition() {} @@ -69,6 +71,11 @@ public: m_end = end; } + bool operator==(const GTextRange& other) const + { + return m_start == other.m_start && m_end == other.m_end; + } + private: GTextPosition normalized_start() const { return m_start < m_end ? m_start : m_end; } GTextPosition normalized_end() const { return m_start < m_end ? m_end : m_start; } @@ -108,6 +115,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_spacing() const { return m_line_spacing; } int line_height() const { return font().glyph_height() + m_line_spacing; } @@ -118,8 +126,13 @@ public: bool write_to_file(const StringView& path); + GTextRange find(const StringView&, const GTextPosition& start = {}); + GTextPosition next_position_after(const GTextPosition&, ShouldWrapAtEndOfDocument = ShouldWrapAtEndOfDocument::Yes); + bool has_selection() const { return m_selection.is_valid(); } String selected_text() const; + void set_selection(const GTextRange&); + String text() const; void clear(); @@ -192,6 +205,7 @@ private: 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(); void set_cursor(int line, int column); void set_cursor(const GTextPosition&); @@ -207,6 +221,7 @@ private: void delete_selection(); void did_update_selection(); int content_x_for_position(const GTextPosition&) const; + char character_at(const GTextPosition&) const; Type m_type { MultiLine }; @@ -232,3 +247,18 @@ private: RefPtr<GAction> m_delete_action; CElapsedTimer m_triple_click_timer; }; + +inline const LogStream& operator<<(const LogStream& stream, const GTextPosition& value) +{ + if (!value.is_valid()) + return stream << "GTextPosition(Invalid)"; + return stream << String::format("(%d,%d)", value.line(), value.column()); +} + +inline const LogStream& operator<<(const LogStream& stream, const GTextRange& value) +{ + if (!value.is_valid()) + return stream << "GTextRange(Invalid)"; + return stream << value.start() << '-' << value.end(); +} + |