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