Serenity Operating System
at hosted 237 lines 8.0 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 <LibWeb/DOM/Document.h> 33#include <LibWeb/Layout/LayoutBlock.h> 34#include <LibWeb/Layout/LayoutText.h> 35#include <ctype.h> 36 37namespace Web { 38 39LayoutText::LayoutText(const Text& text) 40 : LayoutNode(&text) 41{ 42 set_inline(true); 43} 44 45LayoutText::~LayoutText() 46{ 47} 48 49static bool is_all_whitespace(const String& string) 50{ 51 for (size_t i = 0; i < string.length(); ++i) { 52 if (!isspace(string[i])) 53 return false; 54 } 55 return true; 56} 57 58const String& LayoutText::text_for_style(const StyleProperties& style) const 59{ 60 static String one_space = " "; 61 if (is_all_whitespace(node().data())) { 62 if (style.string_or_fallback(CSS::PropertyID::WhiteSpace, "normal") == "normal") 63 return one_space; 64 } 65 return node().data(); 66} 67 68void LayoutText::render_fragment(RenderingContext& context, const LineBoxFragment& fragment) const 69{ 70 auto& painter = context.painter(); 71 painter.set_font(style().font()); 72 73 auto background_color = style().property(CSS::PropertyID::BackgroundColor); 74 if (background_color.has_value() && background_color.value()->is_color()) 75 painter.fill_rect(enclosing_int_rect(fragment.rect()), background_color.value()->to_color(document())); 76 77 auto color = style().color_or_fallback(CSS::PropertyID::Color, document(), context.palette().base_text()); 78 auto text_decoration = style().string_or_fallback(CSS::PropertyID::TextDecoration, "none"); 79 80 if (document().inspected_node() == &node()) 81 context.painter().draw_rect(enclosing_int_rect(fragment.rect()), Color::Magenta); 82 83 bool is_underline = text_decoration == "underline"; 84 if (is_underline) 85 painter.draw_line(enclosing_int_rect(fragment.rect()).bottom_left().translated(0, 1), enclosing_int_rect(fragment.rect()).bottom_right().translated(0, 1), color); 86 87 painter.draw_text(enclosing_int_rect(fragment.rect()), m_text_for_rendering.substring_view(fragment.start(), fragment.length()), Gfx::TextAlignment::TopLeft, color); 88} 89 90template<typename Callback> 91void LayoutText::for_each_word(Callback callback) const 92{ 93 Utf8View view(m_text_for_rendering); 94 if (view.is_empty()) 95 return; 96 97 auto start_of_word = view.begin(); 98 99 auto commit_word = [&](auto it) { 100 int start = view.byte_offset_of(start_of_word); 101 int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_word); 102 103 if (length > 0) { 104 callback(view.substring_view(start, length), start, length); 105 } 106 107 start_of_word = it; 108 }; 109 110 bool last_was_space = isspace(*view.begin()); 111 112 for (auto it = view.begin(); it != view.end();) { 113 bool is_space = isspace(*it); 114 if (is_space == last_was_space) { 115 ++it; 116 continue; 117 } 118 last_was_space = is_space; 119 commit_word(it); 120 ++it; 121 } 122 if (start_of_word != view.end()) 123 commit_word(view.end()); 124} 125 126void LayoutText::split_preformatted_into_lines(LayoutBlock& container) 127{ 128 auto& font = style().font(); 129 auto& line_boxes = container.line_boxes(); 130 m_text_for_rendering = node().data(); 131 132 Utf8View view(m_text_for_rendering); 133 if (view.is_empty()) 134 return; 135 136 auto start_of_line = view.begin(); 137 138 auto commit_line = [&](auto it) { 139 int start = view.byte_offset_of(start_of_line); 140 int length = view.byte_offset_of(it) - view.byte_offset_of(start_of_line); 141 if (length > 0) 142 line_boxes.last().add_fragment(*this, start, length, font.width(view), font.glyph_height()); 143 }; 144 145 bool last_was_newline = false; 146 for (auto it = view.begin(); it != view.end();) { 147 bool did_commit = false; 148 if (*it == '\n') { 149 commit_line(it); 150 line_boxes.append(LineBox()); 151 did_commit = true; 152 last_was_newline = true; 153 } else { 154 last_was_newline = false; 155 } 156 ++it; 157 if (did_commit) 158 start_of_line = it; 159 } 160 if (start_of_line != view.end() || last_was_newline) 161 commit_line(view.end()); 162} 163 164void LayoutText::split_into_lines(LayoutBlock& container) 165{ 166 auto& font = style().font(); 167 float space_width = font.glyph_width(' ') + font.glyph_spacing(); 168 169 auto& line_boxes = container.line_boxes(); 170 if (line_boxes.is_empty()) 171 line_boxes.append(LineBox()); 172 float available_width = container.width() - line_boxes.last().width(); 173 174 if (style().string_or_fallback(CSS::PropertyID::WhiteSpace, "normal") == "pre") { 175 split_preformatted_into_lines(container); 176 return; 177 } 178 179 // Collapse whitespace into single spaces 180 auto utf8_view = Utf8View(node().data()); 181 StringBuilder builder(node().data().length()); 182 for (auto it = utf8_view.begin(); it != utf8_view.end(); ++it) { 183 if (!isspace(*it)) { 184 builder.append(utf8_view.as_string().characters_without_null_termination() + utf8_view.byte_offset_of(it), it.codepoint_length_in_bytes()); 185 } else { 186 builder.append(' '); 187 auto prev = it; 188 while (it != utf8_view.end() && isspace(*it)) { 189 prev = it; 190 ++it; 191 } 192 it = prev; 193 } 194 } 195 m_text_for_rendering = builder.to_string(); 196 197 struct Word { 198 Utf8View view; 199 int start; 200 int length; 201 }; 202 Vector<Word> words; 203 204 for_each_word([&](const Utf8View& view, int start, int length) { 205 words.append({ Utf8View(view), start, length }); 206 }); 207 208 for (size_t i = 0; i < words.size(); ++i) { 209 auto& word = words[i]; 210 211 float word_width; 212 bool is_whitespace = isspace(*word.view.begin()); 213 214 if (is_whitespace) 215 word_width = space_width; 216 else 217 word_width = font.width(word.view) + font.glyph_spacing(); 218 219 if (line_boxes.last().width() > 0 && word_width > available_width) { 220 line_boxes.append(LineBox()); 221 available_width = container.width(); 222 } 223 224 if (is_whitespace && line_boxes.last().fragments().is_empty()) 225 continue; 226 227 line_boxes.last().add_fragment(*this, word.start, is_whitespace ? 1 : word.length, word_width, font.glyph_height()); 228 available_width -= word_width; 229 230 if (available_width < 0) { 231 line_boxes.append(LineBox()); 232 available_width = container.width(); 233 } 234 } 235} 236 237}