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