Serenity Operating System
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}