From cc2f35badd4bf689632ac1d5d59d717bd3a946c1 Mon Sep 17 00:00:00 2001 From: Zac Date: Sat, 9 Jan 2021 22:47:48 +1000 Subject: [PATCH] TextEditor: Implement word wrapping Add a new wrapping mode to the TextEditor that will wrap lines at the spaces between words. Replace the previous menubar checkbox 'Wrapping Mode' in HackStudio and the TextEditor with an exclusive submenu which allows switching between 'No wrapping', 'Wrap anywhere' and 'Wrap at words'. 'Wrap anywhere' (the new 'Wrap lines') is still the default mode. Setting the wrapping mode in the constructors of the TextEditorWidget and HackStudio has been removed, it is now set when constructing the menubar actions. --- .../TextEditor/TextEditorWidget.cpp | 30 ++++++++++---- .../TextEditor/TextEditorWidget.h | 6 ++- .../DevTools/HackStudio/EditorWrapper.cpp | 1 - .../DevTools/HackStudio/HackStudioWidget.cpp | 29 ++++++++++--- .../DevTools/HackStudio/HackStudioWidget.h | 6 +++ Userland/Libraries/LibGUI/EditingEngine.cpp | 20 ++++----- Userland/Libraries/LibGUI/TextEditor.cpp | 41 +++++++++++++------ Userland/Libraries/LibGUI/TextEditor.h | 13 ++++-- 8 files changed, 105 insertions(+), 41 deletions(-) diff --git a/Userland/Applications/TextEditor/TextEditorWidget.cpp b/Userland/Applications/TextEditor/TextEditorWidget.cpp index 9d31b106f8..6deb939c2f 100644 --- a/Userland/Applications/TextEditor/TextEditorWidget.cpp +++ b/Userland/Applications/TextEditor/TextEditorWidget.cpp @@ -70,7 +70,6 @@ TextEditorWidget::TextEditorWidget() m_editor = *find_descendant_of_type_named("editor"); m_editor->set_ruler_visible(true); m_editor->set_automatic_indentation_enabled(true); - m_editor->set_line_wrapping_enabled(true); m_editor->set_editing_engine(make()); m_editor->on_change = [this] { @@ -364,11 +363,6 @@ TextEditorWidget::TextEditorWidget() m_save_as_action->activate(); }); - m_line_wrapping_setting_action = GUI::Action::create_checkable("Line wrapping", [&](auto& action) { - m_editor->set_line_wrapping_enabled(action.is_checked()); - }); - m_line_wrapping_setting_action->set_checked(m_editor->is_line_wrapping_enabled()); - auto menubar = GUI::MenuBar::construct(); auto& app_menu = menubar->add_menu("Text Editor"); app_menu.add_action(*m_new_action); @@ -434,7 +428,29 @@ TextEditorWidget::TextEditorWidget() })); view_menu.add_separator(); - view_menu.add_action(*m_line_wrapping_setting_action); + + m_wrapping_mode_actions.set_exclusive(true); + auto& wrapping_mode_menu = view_menu.add_submenu("Wrapping mode"); + m_no_wrapping_action = GUI::Action::create_checkable("No wrapping", [&](auto&) { + m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::NoWrap); + }); + m_wrap_anywhere_action = GUI::Action::create_checkable("Wrap anywhere", [&](auto&) { + m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAnywhere); + }); + m_wrap_at_words_action = GUI::Action::create_checkable("Wrap at words", [&](auto&) { + m_editor->set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAtWords); + }); + + m_wrapping_mode_actions.add_action(*m_no_wrapping_action); + m_wrapping_mode_actions.add_action(*m_wrap_anywhere_action); + m_wrapping_mode_actions.add_action(*m_wrap_at_words_action); + + wrapping_mode_menu.add_action(*m_no_wrapping_action); + wrapping_mode_menu.add_action(*m_wrap_anywhere_action); + wrapping_mode_menu.add_action(*m_wrap_at_words_action); + + m_wrap_anywhere_action->set_checked(true); + view_menu.add_separator(); view_menu.add_action(*m_no_preview_action); view_menu.add_action(*m_markdown_preview_action); diff --git a/Userland/Applications/TextEditor/TextEditorWidget.h b/Userland/Applications/TextEditor/TextEditorWidget.h index 763087b9e4..8359df6fbe 100644 --- a/Userland/Applications/TextEditor/TextEditorWidget.h +++ b/Userland/Applications/TextEditor/TextEditorWidget.h @@ -75,7 +75,6 @@ private: RefPtr m_save_action; RefPtr m_save_as_action; RefPtr m_find_replace_action; - RefPtr m_line_wrapping_setting_action; RefPtr m_vim_emulation_setting_action; RefPtr m_find_next_action; @@ -104,6 +103,11 @@ private: RefPtr m_find_widget; RefPtr m_replace_widget; + GUI::ActionGroup m_wrapping_mode_actions; + RefPtr m_no_wrapping_action; + RefPtr m_wrap_anywhere_action; + RefPtr m_wrap_at_words_action; + GUI::ActionGroup syntax_actions; RefPtr m_plain_text_highlight; RefPtr m_cpp_highlight; diff --git a/Userland/DevTools/HackStudio/EditorWrapper.cpp b/Userland/DevTools/HackStudio/EditorWrapper.cpp index e4b6a346c7..40771b4d61 100644 --- a/Userland/DevTools/HackStudio/EditorWrapper.cpp +++ b/Userland/DevTools/HackStudio/EditorWrapper.cpp @@ -56,7 +56,6 @@ EditorWrapper::EditorWrapper() m_editor = add(); m_editor->set_ruler_visible(true); - m_editor->set_line_wrapping_enabled(true); m_editor->set_automatic_indentation_enabled(true); m_editor->on_cursor_change = [this] { diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.cpp b/Userland/DevTools/HackStudio/HackStudioWidget.cpp index f1f5c73d74..06c62c4510 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.cpp +++ b/Userland/DevTools/HackStudio/HackStudioWidget.cpp @@ -875,13 +875,30 @@ void HackStudioWidget::create_edit_menubar(GUI::MenuBar& menubar) edit_menu.add_separator(); - auto line_wrapping_action = GUI::Action::create_checkable("Line wrapping", [this](auto& action) { - for (auto& wrapper : m_all_editor_wrappers) { - wrapper.editor().set_line_wrapping_enabled(action.is_checked()); - } + m_wrapping_mode_actions.set_exclusive(true); + auto& wrapping_mode_menu = edit_menu.add_submenu("Wrapping mode"); + m_no_wrapping_action = GUI::Action::create_checkable("No wrapping", [&](auto&) { + for (auto& wrapper : m_all_editor_wrappers) + wrapper.editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::NoWrap); }); - line_wrapping_action->set_checked(current_editor().is_line_wrapping_enabled()); - edit_menu.add_action(line_wrapping_action); + m_wrap_anywhere_action = GUI::Action::create_checkable("Wrap anywhere", [&](auto&) { + for (auto& wrapper : m_all_editor_wrappers) + wrapper.editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAnywhere); + }); + m_wrap_at_words_action = GUI::Action::create_checkable("Wrap at words", [&](auto&) { + for (auto& wrapper : m_all_editor_wrappers) + wrapper.editor().set_wrapping_mode(GUI::TextEditor::WrappingMode::WrapAtWords); + }); + + m_wrapping_mode_actions.add_action(*m_no_wrapping_action); + m_wrapping_mode_actions.add_action(*m_wrap_anywhere_action); + m_wrapping_mode_actions.add_action(*m_wrap_at_words_action); + + wrapping_mode_menu.add_action(*m_no_wrapping_action); + wrapping_mode_menu.add_action(*m_wrap_anywhere_action); + wrapping_mode_menu.add_action(*m_wrap_at_words_action); + + m_wrap_anywhere_action->set_checked(true); edit_menu.add_separator(); diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.h b/Userland/DevTools/HackStudio/HackStudioWidget.h index 3723ba4874..e5728bfd17 100644 --- a/Userland/DevTools/HackStudio/HackStudioWidget.h +++ b/Userland/DevTools/HackStudio/HackStudioWidget.h @@ -39,6 +39,7 @@ #include "Project.h" #include "ProjectFile.h" #include "TerminalWrapper.h" +#include #include #include #include @@ -169,5 +170,10 @@ private: RefPtr m_debug_action; RefPtr m_build_action; RefPtr m_run_action; + + GUI::ActionGroup m_wrapping_mode_actions; + RefPtr m_no_wrapping_action; + RefPtr m_wrap_anywhere_action; + RefPtr m_wrap_at_words_action; }; } diff --git a/Userland/Libraries/LibGUI/EditingEngine.cpp b/Userland/Libraries/LibGUI/EditingEngine.cpp index bb82a9a6a3..3e16b1c3b1 100644 --- a/Userland/Libraries/LibGUI/EditingEngine.cpp +++ b/Userland/Libraries/LibGUI/EditingEngine.cpp @@ -244,7 +244,7 @@ void EditingEngine::move_to_line_beginning(const KeyEvent& event) { TextPosition new_cursor; m_editor->toggle_selection_if_needed_for_event(event.shift()); - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { // FIXME: Replicate the first_nonspace_column behavior in wrapping mode. auto home_position = m_editor->cursor_content_rect().location().translated(-m_editor->width(), 0); new_cursor = m_editor->text_position_at_content_position(home_position); @@ -262,7 +262,7 @@ void EditingEngine::move_to_line_beginning(const KeyEvent& event) void EditingEngine::move_to_line_end(const KeyEvent& event) { TextPosition new_cursor; - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { auto end_position = m_editor->cursor_content_rect().location().translated(m_editor->width(), 0); new_cursor = m_editor->text_position_at_content_position(end_position); } else { @@ -274,13 +274,13 @@ void EditingEngine::move_to_line_end(const KeyEvent& event) void EditingEngine::move_one_up(const KeyEvent& event) { - if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) { + if (m_editor->cursor().line() > 0 || m_editor->is_wrapping_enabled()) { if (event.ctrl() && event.shift()) { move_selected_lines_up(); return; } TextPosition new_cursor; - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { auto position_above = m_editor->cursor_content_rect().location().translated(0, -m_editor->line_height()); new_cursor = m_editor->text_position_at_content_position(position_above); } else { @@ -295,13 +295,13 @@ void EditingEngine::move_one_up(const KeyEvent& event) void EditingEngine::move_one_down(const KeyEvent& event) { - if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) { + if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_wrapping_enabled()) { if (event.ctrl() && event.shift()) { move_selected_lines_down(); return; } TextPosition new_cursor; - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { new_cursor = m_editor->text_position_at_content_position(m_editor->cursor_content_rect().location().translated(0, m_editor->line_height())); auto position_below = m_editor->cursor_content_rect().location().translated(0, m_editor->line_height()); new_cursor = m_editor->text_position_at_content_position(position_below); @@ -317,11 +317,11 @@ void EditingEngine::move_one_down(const KeyEvent& event) void EditingEngine::move_up(const KeyEvent& event, double page_height_factor) { - if (m_editor->cursor().line() > 0 || m_editor->is_line_wrapping_enabled()) { + if (m_editor->cursor().line() > 0 || m_editor->is_wrapping_enabled()) { int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor); TextPosition new_cursor; - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { auto position_above = m_editor->cursor_content_rect().location().translated(0, -pixels); new_cursor = m_editor->text_position_at_content_position(position_above); } else { @@ -337,10 +337,10 @@ void EditingEngine::move_up(const KeyEvent& event, double page_height_factor) void EditingEngine::move_down(const KeyEvent& event, double page_height_factor) { - if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_line_wrapping_enabled()) { + if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_wrapping_enabled()) { int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor); TextPosition new_cursor; - if (m_editor->is_line_wrapping_enabled()) { + if (m_editor->is_wrapping_enabled()) { auto position_below = m_editor->cursor_content_rect().location().translated(0, pixels); new_cursor = m_editor->text_position_at_content_position(position_below); } else { diff --git a/Userland/Libraries/LibGUI/TextEditor.cpp b/Userland/Libraries/LibGUI/TextEditor.cpp index 55be2949e6..c5d9197c33 100644 --- a/Userland/Libraries/LibGUI/TextEditor.cpp +++ b/Userland/Libraries/LibGUI/TextEditor.cpp @@ -157,7 +157,7 @@ TextPosition TextEditor::text_position_at_content_position(const Gfx::IntPoint& size_t line_index = 0; - if (is_line_wrapping_enabled()) { + if (is_wrapping_enabled()) { for (size_t i = 0; i < line_count(); ++i) { auto& rect = m_line_visual_data[i].visual_rect; if (position.y() >= rect.top() && position.y() <= rect.bottom()) { @@ -198,7 +198,7 @@ TextPosition TextEditor::text_position_at_content_position(const Gfx::IntPoint& break; case Gfx::TextAlignment::CenterRight: // FIXME: Support right-aligned line wrapping, I guess. - ASSERT(!is_line_wrapping_enabled()); + ASSERT(!is_wrapping_enabled()); column_index = (position.x() - content_x_for_position({ line_index, 0 }) + fixed_glyph_width() / 2) / fixed_glyph_width(); break; default: @@ -872,7 +872,7 @@ int TextEditor::content_x_for_position(const TextPosition& position) const return m_horizontal_content_padding + ((is_single_line() && icon()) ? (icon_size() + icon_padding()) : 0) + x_offset; case Gfx::TextAlignment::CenterRight: // FIXME - ASSERT(!is_line_wrapping_enabled()); + ASSERT(!is_wrapping_enabled()); return content_width() - m_horizontal_content_padding - (line.length() * fixed_glyph_width()) + (position.column() * fixed_glyph_width()); default: ASSERT_NOT_REACHED(); @@ -952,7 +952,7 @@ Gfx::IntRect TextEditor::line_content_rect(size_t line_index) const line_rect.center_vertically_within({ {}, frame_inner_rect().size() }); return line_rect; } - if (is_line_wrapping_enabled()) + if (is_wrapping_enabled()) return m_line_visual_data[line_index].visual_rect; return { content_x_for_position({ line_index, 0 }), @@ -1286,7 +1286,7 @@ void TextEditor::did_update_selection() m_copy_action->set_enabled(has_selection()); if (on_selection_change) on_selection_change(); - if (is_line_wrapping_enabled()) { + if (is_wrapping_enabled()) { // FIXME: Try to repaint less. update(); } @@ -1413,16 +1413,31 @@ void TextEditor::recompute_visual_lines(size_t line_index) int available_width = visible_text_rect_in_inner_coordinates().width(); - if (is_line_wrapping_enabled()) { + if (is_wrapping_enabled()) { int line_width_so_far = 0; + size_t last_whitespace_index = 0; + size_t line_width_since_last_whitespace = 0; auto glyph_spacing = font().glyph_spacing(); for (size_t i = 0; i < line.length(); ++i) { auto code_point = line.code_points()[i]; + if (isspace(code_point)) { + last_whitespace_index = i; + line_width_since_last_whitespace = 0; + } auto glyph_width = font().glyph_or_emoji_width(code_point); + line_width_since_last_whitespace += glyph_width + glyph_spacing; if ((line_width_so_far + glyph_width + glyph_spacing) > available_width) { - visual_data.visual_line_breaks.append(i); - line_width_so_far = glyph_width + glyph_spacing; + if (m_wrapping_mode == WrappingMode::WrapAtWords && last_whitespace_index != 0) { + // Plus 1 to get the first letter of the word. + visual_data.visual_line_breaks.append(last_whitespace_index + 1); + line_width_so_far = line_width_since_last_whitespace; + last_whitespace_index = 0; + line_width_since_last_whitespace = 0; + } else { + visual_data.visual_line_breaks.append(i); + line_width_so_far = glyph_width + glyph_spacing; + } continue; } line_width_so_far += glyph_width + glyph_spacing; @@ -1431,7 +1446,7 @@ void TextEditor::recompute_visual_lines(size_t line_index) visual_data.visual_line_breaks.append(line.length()); - if (is_line_wrapping_enabled()) + if (is_wrapping_enabled()) visual_data.visual_rect = { m_horizontal_content_padding, 0, available_width, static_cast(visual_data.visual_line_breaks.size()) * line_height() }; else visual_data.visual_rect = { m_horizontal_content_padding, 0, font().width(line.view()), line_height() }; @@ -1469,13 +1484,13 @@ void TextEditor::for_each_visual_line(size_t line_index, Callback callback) cons } } -void TextEditor::set_line_wrapping_enabled(bool enabled) +void TextEditor::set_wrapping_mode(WrappingMode mode) { - if (m_line_wrapping_enabled == enabled) + if (m_wrapping_mode == mode) return; - m_line_wrapping_enabled = enabled; - horizontal_scrollbar().set_visible(!m_line_wrapping_enabled); + m_wrapping_mode = mode; + horizontal_scrollbar().set_visible(m_wrapping_mode == WrappingMode::NoWrap); update_content_size(); recompute_all_visual_lines(); update(); diff --git a/Userland/Libraries/LibGUI/TextEditor.h b/Userland/Libraries/LibGUI/TextEditor.h index 956e3cfcce..771c3b7a5e 100644 --- a/Userland/Libraries/LibGUI/TextEditor.h +++ b/Userland/Libraries/LibGUI/TextEditor.h @@ -56,6 +56,12 @@ public: DisplayOnly }; + enum WrappingMode { + NoWrap, + WrapAnywhere, + WrapAtWords + }; + virtual ~TextEditor() override; const TextDocument& document() const { return *m_document; } @@ -80,8 +86,9 @@ public: virtual int soft_tab_width() const final { return m_soft_tab_width; } - bool is_line_wrapping_enabled() const { return m_line_wrapping_enabled; } - void set_line_wrapping_enabled(bool); + WrappingMode wrapping_mode() const { return m_wrapping_mode; } + bool is_wrapping_enabled() const { return m_wrapping_mode != WrappingMode::NoWrap; } + void set_wrapping_mode(WrappingMode); Gfx::TextAlignment text_alignment() const { return m_text_alignment; } void set_text_alignment(Gfx::TextAlignment); @@ -299,7 +306,7 @@ private: bool m_ruler_visible { false }; bool m_has_pending_change_notification { false }; bool m_automatic_indentation_enabled { false }; - bool m_line_wrapping_enabled { false }; + WrappingMode m_wrapping_mode { WrappingMode::WrapAnywhere }; bool m_has_visible_list { false }; bool m_visualize_trailing_whitespace { true }; int m_line_spacing { 4 };