Serenity Operating System
at portability 233 lines 7.9 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include <AK/StringBuilder.h> 28#include <AK/Utf8View.h> 29#include <LibCore/DirIterator.h> 30#include <LibGfx/Font.h> 31#include <LibGUI/Painter.h> 32#include <LibHTML/DOM/Document.h> 33#include <LibHTML/Layout/LayoutBlock.h> 34#include <LibHTML/Layout/LayoutText.h> 35#include <ctype.h> 36 37LayoutText::LayoutText(const Text& text) 38 : LayoutNode(&text) 39{ 40 set_inline(true); 41} 42 43LayoutText::~LayoutText() 44{ 45} 46 47static bool is_all_whitespace(const String& string) 48{ 49 for (size_t i = 0; i < string.length(); ++i) { 50 if (!isspace(string[i])) 51 return false; 52 } 53 return true; 54} 55 56const String& LayoutText::text_for_style(const StyleProperties& style) const 57{ 58 static String one_space = " "; 59 if (is_all_whitespace(node().data())) { 60 if (style.string_or_fallback(CSS::PropertyID::WhiteSpace, "normal") == "normal") 61 return one_space; 62 } 63 return node().data(); 64} 65 66void LayoutText::render_fragment(RenderingContext& context, const LineBoxFragment& fragment) const 67{ 68 auto& painter = context.painter(); 69 painter.set_font(style().font()); 70 71 auto background_color = style().property(CSS::PropertyID::BackgroundColor); 72 if (background_color.has_value() && background_color.value()->is_color()) 73 painter.fill_rect(enclosing_int_rect(fragment.rect()), background_color.value()->to_color(document())); 74 75 auto color = style().color_or_fallback(CSS::PropertyID::Color, document(), context.palette().base_text()); 76 auto text_decoration = style().string_or_fallback(CSS::PropertyID::TextDecoration, "none"); 77 78 if (document().inspected_node() == &node()) 79 context.painter().draw_rect(enclosing_int_rect(fragment.rect()), Color::Magenta); 80 81 bool is_underline = text_decoration == "underline"; 82 if (is_underline) 83 painter.draw_line(enclosing_int_rect(fragment.rect()).bottom_left().translated(0, 1), enclosing_int_rect(fragment.rect()).bottom_right().translated(0, 1), color); 84 85 painter.draw_text(enclosing_int_rect(fragment.rect()), m_text_for_rendering.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::TopLeft, color); 86} 87 88template<typename Callback> 89void LayoutText::for_each_word(Callback callback) const 90{ 91 Utf8View view(m_text_for_rendering); 92 if (view.is_empty()) 93 return; 94 95 auto start_of_word = view.begin(); 96 97 auto commit_word = [&](auto it) { 98 int start = view.byte_offset_of(start_of_word); 99 int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_word); 100 101 if (length > 0) { 102 callback(view.substring_view(start, length), start, length); 103 } 104 105 start_of_word = it; 106 }; 107 108 bool last_was_space = isspace(*view.begin()); 109 110 for (auto it = view.begin(); it != view.end();) { 111 bool is_space = isspace(*it); 112 if (is_space == last_was_space) { 113 ++it; 114 continue; 115 } 116 last_was_space = is_space; 117 commit_word(it); 118 ++it; 119 } 120 if (start_of_word != view.end()) 121 commit_word(view.end()); 122} 123 124void LayoutText::split_preformatted_into_lines(LayoutBlock& container) 125{ 126 auto& font = style().font(); 127 auto& line_boxes = container.line_boxes(); 128 m_text_for_rendering = node().data(); 129 130 Utf8View view(m_text_for_rendering); 131 if (view.is_empty()) 132 return; 133 134 auto start_of_line = view.begin(); 135 136 auto commit_line = [&](auto it) { 137 int start = view.byte_offset_of(start_of_line); 138 int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_line); 139 if (length > 0) 140 line_boxes.last().add_fragment(*this, start, length, font.width(view), font.glyph_height()); 141 }; 142 143 bool last_was_newline = false; 144 for (auto it = view.begin(); it != view.end();) { 145 bool did_commit = false; 146 if (*it == '\n') { 147 commit_line(it); 148 line_boxes.append(LineBox()); 149 did_commit = true; 150 last_was_newline = true; 151 } else { 152 last_was_newline = false; 153 } 154 ++it; 155 if (did_commit) 156 start_of_line = it; 157 } 158 if (start_of_line != view.end() || last_was_newline) 159 commit_line(view.end()); 160} 161 162void LayoutText::split_into_lines(LayoutBlock& container) 163{ 164 auto& font = style().font(); 165 float space_width = font.glyph_width(' ') + font.glyph_spacing(); 166 167 auto& line_boxes = container.line_boxes(); 168 if (line_boxes.is_empty()) 169 line_boxes.append(LineBox()); 170 float available_width = container.width() - line_boxes.last().width(); 171 172 if (style().string_or_fallback(CSS::PropertyID::WhiteSpace, "normal") == "pre") { 173 split_preformatted_into_lines(container); 174 return; 175 } 176 177 // Collapse whitespace into single spaces 178 auto utf8_view = Utf8View(node().data()); 179 StringBuilder builder(node().data().length()); 180 for (auto it = utf8_view.begin(); it != utf8_view.end(); ++it) { 181 if (!isspace(*it)) { 182 builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.codepoint_length_in_bytes()); 183 } else { 184 builder.append(' '); 185 auto prev = it; 186 while (it != utf8_view.end() && isspace(*it)) { 187 prev = it; 188 ++it; 189 } 190 it = prev; 191 } 192 } 193 m_text_for_rendering = builder.to_string(); 194 195 struct Word { 196 Utf8View view; 197 int start; 198 int length; 199 }; 200 Vector<Word> words; 201 202 for_each_word([&](const Utf8View& view, int start, int length) { 203 words.append({ Utf8View(view), start, length }); 204 }); 205 206 for (size_t i = 0; i < words.size(); ++i) { 207 auto& word = words[i]; 208 209 float word_width; 210 bool is_whitespace = isspace(*word.view.begin()); 211 212 if (is_whitespace) 213 word_width = space_width; 214 else 215 word_width = font.width(word.view) + font.glyph_spacing(); 216 217 if (line_boxes.last().width() > 0 && word_width > available_width) { 218 line_boxes.append(LineBox()); 219 available_width = container.width(); 220 } 221 222 if (is_whitespace && line_boxes.last().fragments().is_empty()) 223 continue; 224 225 line_boxes.last().add_fragment(*this, word.start, is_whitespace ? 1 : word.length, word_width, font.glyph_height()); 226 available_width -= word_width; 227 228 if (available_width < 0) { 229 line_boxes.append(LineBox()); 230 available_width = container.width(); 231 } 232 } 233}