Bladeren bron

LibGUI: Implement trailing whitespace visualization in TextEditor

This patch adds an optional mode where TextEditor highlights trailing
whitespace characters on each line with a nice reddish dither pattern.

We should probably make this themable and I'm sure it could be nicer
somehow, but this is just a first cut and I do kinda like it. :^)
Andreas Kling 4 jaren geleden
bovenliggende
commit
aa70d8c217

+ 17 - 0
Libraries/LibGUI/TextDocument.cpp

@@ -94,6 +94,23 @@ size_t TextDocumentLine::first_non_whitespace_column() const
     return length();
 }
 
+Optional<size_t> TextDocumentLine::last_non_whitespace_column() const
+{
+    for (ssize_t i = length() - 1; i >= 0; --i) {
+        auto code_point = code_points()[i];
+        if (!isspace(code_point))
+            return i;
+    }
+    return {};
+}
+
+bool TextDocumentLine::ends_in_whitespace() const
+{
+    if (!length())
+        return false;
+    return isspace(code_points()[length() - 1]);
+}
+
 String TextDocumentLine::to_utf8() const
 {
     StringBuilder builder;

+ 2 - 0
Libraries/LibGUI/TextDocument.h

@@ -177,6 +177,8 @@ public:
     void remove_range(TextDocument&, size_t start, size_t length);
 
     size_t first_non_whitespace_column() const;
+    Optional<size_t> last_non_whitespace_column() const;
+    bool ends_in_whitespace() const;
 
 private:
     // NOTE: This vector is null terminated.

+ 29 - 0
Libraries/LibGUI/TextEditor.cpp

@@ -495,6 +495,26 @@ void TextEditor::paint_event(PaintEvent& event)
                 }
             }
 
+            if (m_visualize_trailing_whitespace && line.ends_in_whitespace()) {
+                size_t physical_column;
+                auto last_non_whitespace_column = line.last_non_whitespace_column();
+                if (last_non_whitespace_column.has_value())
+                    physical_column = last_non_whitespace_column.value() + 1;
+                else
+                    physical_column = 0;
+                size_t end_of_visual_line = (start_of_visual_line + visual_line_text.length());
+                if (physical_column < end_of_visual_line) {
+                    size_t visual_column = physical_column > start_of_visual_line ? (physical_column - start_of_visual_line) : 0;
+                    Gfx::IntRect whitespace_rect {
+                        content_x_for_position({ line_index, visual_column }),
+                        visual_line_rect.y(),
+                        font().width(visual_line_text.substring_view(visual_column, visual_line_text.length() - visual_column)),
+                        visual_line_rect.height()
+                    };
+                    painter.fill_rect_with_dither_pattern(whitespace_rect, Color(), Color(255, 192, 192));
+                }
+            }
+
             if (physical_line_has_selection) {
                 size_t start_of_selection_within_visual_line = (size_t)max(0, (int)selection_start_column_within_line - (int)start_of_visual_line);
                 size_t end_of_selection_within_visual_line = selection_end_column_within_line - start_of_visual_line;
@@ -536,6 +556,7 @@ void TextEditor::paint_event(PaintEvent& event)
                     }
                 }
             }
+
             ++visual_line_index;
             return IterationDecision::Continue;
         });
@@ -1718,4 +1739,12 @@ void TextEditor::set_icon(const Gfx::Bitmap* icon)
     update();
 }
 
+void TextEditor::set_visualize_trailing_whitespace(bool enabled)
+{
+    if (m_visualize_trailing_whitespace == enabled)
+        return;
+    m_visualize_trailing_whitespace = enabled;
+    update();
+}
+
 }

+ 4 - 0
Libraries/LibGUI/TextEditor.h

@@ -60,6 +60,9 @@ public:
 
     virtual void set_document(TextDocument&);
 
+    void set_visualize_trailing_whitespace(bool);
+    bool visualize_trailing_whitespace() const { return m_visualize_trailing_whitespace; }
+
     bool has_visible_list() const { return m_has_visible_list; }
     void set_has_visible_list(bool);
     bool has_open_button() const { return m_has_open_button; }
@@ -274,6 +277,7 @@ private:
     bool m_line_wrapping_enabled { false };
     bool m_has_visible_list { false };
     bool m_has_open_button { false };
+    bool m_visualize_trailing_whitespace { true };
     int m_line_spacing { 4 };
     size_t m_soft_tab_width { 4 };
     int m_horizontal_content_padding { 3 };