Serenity Operating System
at portability 386 lines 15 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 <LibGUI/Painter.h> 28#include <LibHTML/CSS/StyleResolver.h> 29#include <LibHTML/DOM/Element.h> 30#include <LibHTML/Layout/LayoutBlock.h> 31#include <LibHTML/Layout/LayoutInline.h> 32#include <LibHTML/Layout/LayoutReplaced.h> 33#include <LibHTML/Layout/LayoutText.h> 34#include <math.h> 35 36LayoutBlock::LayoutBlock(const Node* node, NonnullRefPtr<StyleProperties> style) 37 : LayoutBox(node, move(style)) 38{ 39} 40 41LayoutBlock::~LayoutBlock() 42{ 43} 44 45LayoutNode& LayoutBlock::inline_wrapper() 46{ 47 if (!last_child() || !last_child()->is_block() || last_child()->node() != nullptr) { 48 append_child(adopt(*new LayoutBlock(nullptr, style_for_anonymous_block()))); 49 last_child()->set_children_are_inline(true); 50 } 51 return *last_child(); 52} 53 54void LayoutBlock::layout() 55{ 56 compute_width(); 57 compute_position(); 58 59 if (children_are_inline()) 60 layout_inline_children(); 61 else 62 layout_block_children(); 63 64 compute_height(); 65} 66 67void LayoutBlock::layout_block_children() 68{ 69 ASSERT(!children_are_inline()); 70 float content_height = 0; 71 for_each_child([&](auto& child) { 72 // FIXME: What should we do here? Something like a <table> might have a bunch of useless text children.. 73 if (child.is_inline()) 74 return; 75 auto& child_block = static_cast<LayoutBlock&>(child); 76 child_block.layout(); 77 content_height = child_block.rect().bottom() + child_block.box_model().full_margin().bottom - rect().top(); 78 }); 79 rect().set_height(content_height); 80} 81 82void LayoutBlock::layout_inline_children() 83{ 84 ASSERT(children_are_inline()); 85 m_line_boxes.clear(); 86 for_each_child([&](auto& child) { 87 ASSERT(child.is_inline()); 88 child.split_into_lines(*this); 89 }); 90 91 for (auto& line_box : m_line_boxes) { 92 line_box.trim_trailing_whitespace(); 93 } 94 95 float min_line_height = style().line_height(); 96 float line_spacing = min_line_height - style().font().glyph_height(); 97 float content_height = 0; 98 99 // FIXME: This should be done by the CSS parser! 100 CSS::ValueID text_align = CSS::ValueID::Left; 101 auto text_align_string = style().string_or_fallback(CSS::PropertyID::TextAlign, "left"); 102 if (text_align_string == "center") 103 text_align = CSS::ValueID::Center; 104 else if (text_align_string == "left") 105 text_align = CSS::ValueID::Left; 106 else if (text_align_string == "right") 107 text_align = CSS::ValueID::Right; 108 else if (text_align_string == "justify") 109 text_align = CSS::ValueID::Justify; 110 111 for (auto& line_box : m_line_boxes) { 112 float max_height = min_line_height; 113 for (auto& fragment : line_box.fragments()) { 114 max_height = max(max_height, fragment.rect().height()); 115 } 116 117 float x_offset = x(); 118 float excess_horizontal_space = (float)width() - line_box.width(); 119 120 switch (text_align) { 121 case CSS::ValueID::Center: 122 x_offset += excess_horizontal_space / 2; 123 break; 124 case CSS::ValueID::Right: 125 x_offset += excess_horizontal_space; 126 break; 127 case CSS::ValueID::Left: 128 case CSS::ValueID::Justify: 129 default: 130 break; 131 } 132 133 float excess_horizontal_space_including_whitespace = excess_horizontal_space; 134 int whitespace_count = 0; 135 if (text_align == CSS::ValueID::Justify) { 136 for (auto& fragment : line_box.fragments()) { 137 if (fragment.is_justifiable_whitespace()) { 138 ++whitespace_count; 139 excess_horizontal_space_including_whitespace += fragment.rect().width(); 140 } 141 } 142 } 143 144 float justified_space_width = whitespace_count ? (excess_horizontal_space_including_whitespace / (float)whitespace_count) : 0; 145 146 for (size_t i = 0; i < line_box.fragments().size(); ++i) { 147 auto& fragment = line_box.fragments()[i]; 148 // Vertically align everyone's bottom to the line. 149 // FIXME: Support other kinds of vertical alignment. 150 fragment.rect().set_x(roundf(x_offset + fragment.rect().x())); 151 fragment.rect().set_y(y() + content_height + (max_height - fragment.rect().height()) - (line_spacing / 2)); 152 153 if (text_align == CSS::ValueID::Justify) { 154 if (fragment.is_justifiable_whitespace()) { 155 if (fragment.rect().width() != justified_space_width) { 156 float diff = justified_space_width - fragment.rect().width(); 157 fragment.rect().set_width(justified_space_width); 158 // Shift subsequent sibling fragments to the right to adjust for change in width. 159 for (size_t j = i + 1; j < line_box.fragments().size(); ++j) { 160 line_box.fragments()[j].rect().move_by(diff, 0); 161 } 162 } 163 } 164 } 165 166 if (is<LayoutReplaced>(fragment.layout_node())) 167 const_cast<LayoutReplaced&>(to<LayoutReplaced>(fragment.layout_node())).set_rect(fragment.rect()); 168 169 float final_line_box_width = 0; 170 for (auto& fragment : line_box.fragments()) 171 final_line_box_width += fragment.rect().width(); 172 line_box.m_width = final_line_box_width; 173 } 174 175 content_height += max_height; 176 } 177 178 rect().set_height(content_height); 179} 180 181void LayoutBlock::compute_width() 182{ 183 auto& style = this->style(); 184 185 auto auto_value = Length(); 186 auto zero_value = Length(0, Length::Type::Absolute); 187 188 Length margin_left; 189 Length margin_right; 190 Length border_left; 191 Length border_right; 192 Length padding_left; 193 Length padding_right; 194 195 auto try_compute_width = [&](const auto& a_width) { 196 Length width = a_width; 197#ifdef HTML_DEBUG 198 dbg() << " Left: " << margin_left << "+" << border_left << "+" << padding_left; 199 dbg() << "Right: " << margin_right << "+" << border_right << "+" << padding_right; 200#endif 201 margin_left = style.length_or_fallback(CSS::PropertyID::MarginLeft, zero_value); 202 margin_right = style.length_or_fallback(CSS::PropertyID::MarginRight, zero_value); 203 border_left = style.length_or_fallback(CSS::PropertyID::BorderLeftWidth, zero_value); 204 border_right = style.length_or_fallback(CSS::PropertyID::BorderRightWidth, zero_value); 205 padding_left = style.length_or_fallback(CSS::PropertyID::PaddingLeft, zero_value); 206 padding_right = style.length_or_fallback(CSS::PropertyID::PaddingRight, zero_value); 207 208 float total_px = 0; 209 for (auto& value : { margin_left, border_left, padding_left, width, padding_right, border_right, margin_right }) { 210 total_px += value.to_px(); 211 } 212 213#ifdef HTML_DEBUG 214 dbg() << "Total: " << total_px; 215#endif 216 217 // 10.3.3 Block-level, non-replaced elements in normal flow 218 // If 'width' is not 'auto' and 'border-left-width' + 'padding-left' + 'width' + 'padding-right' + 'border-right-width' (plus any of 'margin-left' or 'margin-right' that are not 'auto') is larger than the width of the containing block, then any 'auto' values for 'margin-left' or 'margin-right' are, for the following rules, treated as zero. 219 if (width.is_auto() && total_px > containing_block()->width()) { 220 if (margin_left.is_auto()) 221 margin_left = zero_value; 222 if (margin_right.is_auto()) 223 margin_right = zero_value; 224 } 225 226 // 10.3.3 cont'd. 227 auto underflow_px = containing_block()->width() - total_px; 228 229 if (width.is_auto()) { 230 if (margin_left.is_auto()) 231 margin_left = zero_value; 232 if (margin_right.is_auto()) 233 margin_right = zero_value; 234 if (underflow_px >= 0) { 235 width = Length(underflow_px, Length::Type::Absolute); 236 } else { 237 width = zero_value; 238 margin_right = Length(margin_right.to_px() + underflow_px, Length::Type::Absolute); 239 } 240 } else { 241 if (!margin_left.is_auto() && !margin_right.is_auto()) { 242 margin_right = Length(margin_right.to_px() + underflow_px, Length::Type::Absolute); 243 } else if (!margin_left.is_auto() && margin_right.is_auto()) { 244 margin_right = Length(underflow_px, Length::Type::Absolute); 245 } else if (margin_left.is_auto() && !margin_right.is_auto()) { 246 margin_left = Length(underflow_px, Length::Type::Absolute); 247 } else { // margin_left.is_auto() && margin_right.is_auto() 248 auto half_of_the_underflow = Length(underflow_px / 2, Length::Type::Absolute); 249 margin_left = half_of_the_underflow; 250 margin_right = half_of_the_underflow; 251 } 252 } 253 return width; 254 }; 255 256 auto specified_width = style.length_or_fallback(CSS::PropertyID::Width, auto_value); 257 258 // 1. The tentative used width is calculated (without 'min-width' and 'max-width') 259 auto used_width = try_compute_width(specified_width); 260 261 // 2. The tentative used width is greater than 'max-width', the rules above are applied again, 262 // but this time using the computed value of 'max-width' as the computed value for 'width'. 263 auto specified_max_width = style.length_or_fallback(CSS::PropertyID::MaxWidth, auto_value); 264 if (!specified_max_width.is_auto()) { 265 if (used_width.to_px() > specified_max_width.to_px()) { 266 used_width = try_compute_width(specified_max_width); 267 } 268 } 269 270 // 3. If the resulting width is smaller than 'min-width', the rules above are applied again, 271 // but this time using the value of 'min-width' as the computed value for 'width'. 272 auto specified_min_width = style.length_or_fallback(CSS::PropertyID::MinWidth, auto_value); 273 if (!specified_min_width.is_auto()) { 274 if (used_width.to_px() < specified_min_width.to_px()) { 275 used_width = try_compute_width(specified_min_width); 276 } 277 } 278 279 rect().set_width(used_width.to_px()); 280 box_model().margin().left = margin_left; 281 box_model().margin().right = margin_right; 282 box_model().border().left = border_left; 283 box_model().border().right = border_right; 284 box_model().padding().left = padding_left; 285 box_model().padding().right = padding_right; 286} 287 288void LayoutBlock::compute_position() 289{ 290 auto& style = this->style(); 291 292 auto auto_value = Length(); 293 auto zero_value = Length(0, Length::Type::Absolute); 294 295 auto width = style.length_or_fallback(CSS::PropertyID::Width, auto_value); 296 297 box_model().margin().top = style.length_or_fallback(CSS::PropertyID::MarginTop, zero_value); 298 box_model().margin().bottom = style.length_or_fallback(CSS::PropertyID::MarginBottom, zero_value); 299 box_model().border().top = style.length_or_fallback(CSS::PropertyID::BorderTopWidth, zero_value); 300 box_model().border().bottom = style.length_or_fallback(CSS::PropertyID::BorderBottomWidth, zero_value); 301 box_model().padding().top = style.length_or_fallback(CSS::PropertyID::PaddingTop, zero_value); 302 box_model().padding().bottom = style.length_or_fallback(CSS::PropertyID::PaddingBottom, zero_value); 303 rect().set_x(containing_block()->x() + box_model().margin().left.to_px() + box_model().border().left.to_px() + box_model().padding().left.to_px()); 304 305 float top_border = -1; 306 if (previous_sibling() != nullptr) { 307 auto& previous_sibling_rect = previous_sibling()->rect(); 308 auto& previous_sibling_style = previous_sibling()->box_model(); 309 top_border = previous_sibling_rect.y() + previous_sibling_rect.height(); 310 top_border += previous_sibling_style.full_margin().bottom; 311 } else { 312 top_border = containing_block()->y(); 313 } 314 rect().set_y(top_border + box_model().full_margin().top); 315} 316 317void LayoutBlock::compute_height() 318{ 319 auto& style = this->style(); 320 321 auto height_property = style.property(CSS::PropertyID::Height); 322 if (!height_property.has_value()) 323 return; 324 auto height_length = height_property.value()->to_length(); 325 if (height_length.is_absolute()) 326 rect().set_height(height_length.to_px()); 327} 328 329void LayoutBlock::render(RenderingContext& context) 330{ 331 if (!is_visible()) 332 return; 333 334 LayoutBox::render(context); 335 336 if (children_are_inline()) { 337 for (auto& line_box : m_line_boxes) { 338 for (auto& fragment : line_box.fragments()) { 339 if (context.should_show_line_box_borders()) 340 context.painter().draw_rect(enclosing_int_rect(fragment.rect()), Color::Green); 341 fragment.render(context); 342 } 343 } 344 } 345} 346 347HitTestResult LayoutBlock::hit_test(const Gfx::Point& position) const 348{ 349 if (!children_are_inline()) 350 return LayoutBox::hit_test(position); 351 352 HitTestResult result; 353 for (auto& line_box : m_line_boxes) { 354 for (auto& fragment : line_box.fragments()) { 355 if (enclosing_int_rect(fragment.rect()).contains(position)) { 356 return { fragment.layout_node(), fragment.text_index_at(position.x()) }; 357 } 358 } 359 } 360 return {}; 361} 362 363NonnullRefPtr<StyleProperties> LayoutBlock::style_for_anonymous_block() const 364{ 365 auto new_style = StyleProperties::create(); 366 367 style().for_each_property([&](auto property_id, auto& value) { 368 if (StyleResolver::is_inherited_property(property_id)) 369 new_style->set_property(property_id, value); 370 }); 371 372 return new_style; 373} 374 375LineBox& LayoutBlock::ensure_last_line_box() 376{ 377 if (m_line_boxes.is_empty()) 378 m_line_boxes.append(LineBox()); 379 return m_line_boxes.last(); 380} 381 382LineBox& LayoutBlock::add_line_box() 383{ 384 m_line_boxes.append(LineBox()); 385 return m_line_boxes.last(); 386}