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 };