#include "html.h" #include "line_box.h" #include "element.h" #include "render_item.h" #include ////////////////////////////////////////////////////////////////////////////////////////// litehtml::line_box_item::~line_box_item() = default; void litehtml::line_box_item::place_to(int x, int y) { m_element->pos().x = x + m_element->content_offset_left(); m_element->pos().y = y + m_element->content_offset_top(); } litehtml::position& litehtml::line_box_item::pos() { return m_element->pos(); } int litehtml::line_box_item::width() const { return m_element->width(); } int litehtml::line_box_item::top() const { return m_element->top(); } int litehtml::line_box_item::bottom() const { return m_element->bottom(); } int litehtml::line_box_item::right() const { return m_element->right(); } int litehtml::line_box_item::left() const { return m_element->left(); } int litehtml::line_box_item::height() const { return m_element->height(); } ////////////////////////////////////////////////////////////////////////////////////////// litehtml::lbi_start::lbi_start(const std::shared_ptr& element) : line_box_item(element) { m_pos.height = m_element->src_el()->css().get_font_metrics().height; m_pos.width = m_element->content_offset_left(); } litehtml::lbi_start::~lbi_start() = default; void litehtml::lbi_start::place_to(int x, int y) { m_pos.x = x + m_element->content_offset_left(); m_pos.y = y; } int litehtml::lbi_start::width() const { return m_pos.width; } int litehtml::lbi_start::top() const { return m_pos.y; } int litehtml::lbi_start::bottom() const { return m_pos.y + m_pos.height; } int litehtml::lbi_start::right() const { return m_pos.x; } int litehtml::lbi_start::left() const { return m_pos.x - m_element->content_offset_left(); } int litehtml::lbi_start::height() const { return m_pos.height; } ////////////////////////////////////////////////////////////////////////////////////////// litehtml::lbi_end::lbi_end(const std::shared_ptr& element) : lbi_start(element) { m_pos.height = m_element->src_el()->css().get_font_metrics().height; m_pos.width = m_element->content_offset_right(); } litehtml::lbi_end::~lbi_end() = default; void litehtml::lbi_end::place_to(int x, int y) { m_pos.x = x; m_pos.y = y; } int litehtml::lbi_end::right() const { return m_pos.x + m_pos.width; } int litehtml::lbi_end::left() const { return m_pos.x; } ////////////////////////////////////////////////////////////////////////////////////////// litehtml::lbi_continue::lbi_continue(const std::shared_ptr& element) : lbi_start(element) { m_pos.height = m_element->src_el()->css().get_font_metrics().height; m_pos.width = 0; } litehtml::lbi_continue::~lbi_continue() = default; void litehtml::lbi_continue::place_to(int x, int y) { m_pos.x = x; m_pos.y = y; } int litehtml::lbi_continue::right() const { return m_pos.x; } int litehtml::lbi_continue::left() const { return m_pos.x; } int litehtml::lbi_continue::width() const { return 0; } ////////////////////////////////////////////////////////////////////////////////////////// void litehtml::line_box::add_item(std::unique_ptr item) { item->get_el()->skip(false); bool add = true; switch (item->get_type()) { case line_box_item::type_text_part: if(item->get_el()->src_el()->is_white_space()) { add = !is_empty() && !have_last_space(); } break; case line_box_item::type_inline_start: case line_box_item::type_inline_end: case line_box_item::type_inline_continue: add = true; break; } if(add) { item->place_to(m_left + m_width, m_top); m_width += item->width(); m_height = std::max(m_height, item->get_el()->height()); m_items.emplace_back(std::move(item)); } else { item->get_el()->skip(true); } } int litehtml::line_box::calc_va_baseline(const va_context& current, vertical_align va, const font_metrics& new_font, int top, int bottom) { switch(va) { case va_super: return current.baseline - current.fm.super_shift; case va_sub: return current.baseline + current.fm.sub_shift; case va_middle: return current.baseline - current.fm.x_height / 2; case va_text_top: return current.baseline - (current.fm.height - current.fm.base_line()) + new_font.height - new_font.base_line(); case va_text_bottom: return current.baseline + current.fm.base_line() - new_font.base_line(); case va_bottom: return bottom - new_font.base_line(); case va_top: return top + new_font.height - new_font.base_line(); default: return current.baseline; } } std::list< std::unique_ptr > litehtml::line_box::finish(bool last_box, const containing_block_context &containing_block_size) { std::list< std::unique_ptr > ret_items; if(!last_box) { while(!m_items.empty()) { if (m_items.back()->get_type() == line_box_item::type_text_part) { // remove trailing spaces if (m_items.back()->get_el()->src_el()->is_break() || m_items.back()->get_el()->src_el()->is_white_space()) { m_width -= m_items.back()->width(); m_items.back()->get_el()->skip(true); m_items.pop_back(); } else { break; } } else if (m_items.back()->get_type() == line_box_item::type_inline_start) { // remove trailing empty inline_start markers // these markers will be added at the beginning of the next line box m_width -= m_items.back()->width(); ret_items.emplace_back(std::move(m_items.back())); m_items.pop_back(); } else { break; } } } else { // remove trailing spaces auto iter = m_items.rbegin(); while(iter != m_items.rend()) { if ((*iter)->get_type() == line_box_item::type_text_part) { if((*iter)->get_el()->src_el()->is_white_space()) { (*iter)->get_el()->skip(true); m_width -= (*iter)->width(); // Space can be between text and inline_end marker // We have to shift all items on the right side if(iter != m_items.rbegin()) { auto r_iter = iter; r_iter--; while (true) { (*r_iter)->pos().x -= (*iter)->width(); if (r_iter == m_items.rbegin()) { break; } r_iter--; } } // erase white space element iter = decltype(iter) (m_items.erase( std::next(iter).base() )); } else { break; } } else { iter++; } } } if( is_empty() || (!is_empty() && last_box && is_break_only()) ) { m_height = m_default_line_height.computed_value; m_baseline = m_font_metrics.base_line(); return ret_items; } int spacing_x = 0; // Number of pixels to distribute between elements int shift_x = 0; // Shift elements by X to apply the text-align switch(m_text_align) { case text_align_right: if(m_width < (m_right - m_left)) { shift_x = (m_right - m_left) - m_width; } break; case text_align_center: if(m_width < (m_right - m_left)) { shift_x = ((m_right - m_left) - m_width) / 2; } break; case text_align_justify: if (m_width < (m_right - m_left)) { shift_x = 0; spacing_x = (m_right - m_left) - m_width; // don't justify for small lines if (spacing_x > m_width / 4) spacing_x = 0; } break; default: shift_x = 0; } int counter = 0; float offj = float(spacing_x) / std::max(1.f, float(m_items.size()) - 1.f); float cixx = 0.0f; std::optional line_height; if(!m_default_line_height.css_value.is_predefined()) { line_height = m_default_line_height.computed_value; } va_context current_context; std::list contexts; current_context.baseline = 0; current_context.fm = m_font_metrics; current_context.start_lbi = nullptr; current_context.line_height = m_default_line_height.computed_value; m_min_width = 0; struct items_dimensions { int top = 0; int bottom = 0; int count = 0; int max_height = 0; void add_item(const line_box_item* item) { top = std::min(top, item->top()); bottom = std::max(bottom, item->bottom()); max_height = std::max(max_height, item->height()); count++; } int height() const { return bottom - top; } }; items_dimensions line_max_height; items_dimensions top_aligned_max_height; items_dimensions bottom_aligned_max_height; items_dimensions inline_boxes_dims; // First pass // 1. Apply text-align-justify // 1. Align all items by baseline // 2. top/button aligned items are aligned by baseline // 3. Calculate top and button of the linebox separately for items in baseline // and for top and bottom aligned items for (const auto& lbi : m_items) { // Apply text-align-justify m_min_width += lbi->get_rendered_min_width(); if (spacing_x && counter) { cixx += offj; if ((counter + 1) == int(m_items.size())) cixx += 0.99f; lbi->pos().x += int(cixx); } counter++; if ((m_text_align == text_align_right || spacing_x) && counter == int(m_items.size())) { // Forcible justify the last element to the right side for text align right and justify; lbi->pos().x = m_right - lbi->pos().width; } else if (shift_x) { lbi->pos().x += shift_x; } // Calculate new baseline for inline start/continue // Inline start/continue elements are inline containers like if (lbi->get_type() == line_box_item::type_inline_start || lbi->get_type() == line_box_item::type_inline_continue) { contexts.push_back(current_context); if(is_one_of(lbi->get_el()->css().get_vertical_align(), va_top, va_bottom)) { // top/bottom aligned inline boxes are aligned by baseline == 0 current_context.baseline = 0; current_context.start_lbi = lbi.get(); current_context.start_lbi->reset_items_height(); } else if(current_context.start_lbi) { current_context.baseline = calc_va_baseline(current_context, lbi->get_el()->css().get_vertical_align(), lbi->get_el()->css().get_font_metrics(), current_context.start_lbi->top(), current_context.start_lbi->bottom()); } else { current_context.start_lbi = nullptr; current_context.baseline = calc_va_baseline(current_context, lbi->get_el()->css().get_vertical_align(), lbi->get_el()->css().get_font_metrics(), line_max_height.top, line_max_height.bottom); } current_context.fm = lbi->get_el()->css().get_font_metrics(); current_context.line_height = lbi->get_el()->css().line_height().computed_value; } int bl = current_context.baseline; int content_offset = 0; bool is_top_bottom_box = false; bool ignore = false; // Align element by baseline if(!is_one_of(lbi->get_el()->src_el()->css().get_display(), display_inline_text, display_inline)) { // Apply margins, paddings and border for inline boxes content_offset = lbi->get_el()->content_offset_top(); switch (lbi->get_el()->css().get_vertical_align()) { case va_bottom: case va_top: // Align by base line 0 all inline boxes with top and bottom vertical aling bl = 0; is_top_bottom_box = true; break; case va_text_bottom: lbi->pos().y = bl + current_context.fm.base_line() - lbi->get_el()->height() + content_offset; ignore = true; break; case va_text_top: lbi->pos().y = bl - current_context.fm.ascent + content_offset; ignore = true; break; case va_middle: lbi->pos().y = bl - current_context.fm.x_height / 2 - lbi->get_el()->height() / 2 + content_offset; ignore = true; break; default: bl = calc_va_baseline(current_context, lbi->get_el()->css().get_vertical_align(), lbi->get_el()->css().get_font_metrics(), line_max_height.top, line_max_height.bottom); break; } } if(!ignore) { lbi->pos().y = bl - lbi->get_el()->get_last_baseline() + content_offset; } if(is_top_bottom_box) { switch (lbi->get_el()->css().get_vertical_align()) { case va_top: top_aligned_max_height.add_item(lbi.get()); break; case va_bottom: bottom_aligned_max_height.add_item(lbi.get()); break; default: break; } } else if(current_context.start_lbi) { current_context.start_lbi->add_item_height(lbi->top(), lbi->bottom()); switch (current_context.start_lbi->get_el()->css().get_vertical_align()) { case va_top: top_aligned_max_height.add_item(lbi.get()); break; case va_bottom: bottom_aligned_max_height.add_item(lbi.get()); break; default: break; } } else { if(!lbi->get_el()->src_el()->is_inline_box()) { line_max_height.add_item(lbi.get()); } else { inline_boxes_dims.add_item(lbi.get()); } } if(!lbi->get_el()->src_el()->is_inline_box() && !lbi->get_el()->css().line_height().css_value.is_predefined()) { if(line_height.has_value()) { line_height = std::max(line_height.value(), lbi->get_el()->css().line_height().computed_value); } else { line_height = lbi->get_el()->css().line_height().computed_value; } } if (lbi->get_type() == line_box_item::type_inline_end) { if(!contexts.empty()) { current_context = contexts.back(); contexts.pop_back(); } } } int top_shift = 0; if(line_height.has_value()) { m_height = line_height.value(); if(line_max_height.count != 0) { // We have inline items top_shift = std::abs(line_max_height.top); const int top_shift_correction = (line_height.value() - line_max_height.height()) / 2; m_baseline = line_max_height.bottom + top_shift_correction; top_shift += top_shift_correction; if(inline_boxes_dims.count) { const int diff2 = std::abs(inline_boxes_dims.top) - std::abs(top_shift); if(diff2 > 0) { m_height += diff2; top_shift += diff2; m_baseline += diff2; } const int diff1 = inline_boxes_dims.bottom - (line_max_height.bottom + top_shift_correction); if(diff1 > 0) { m_height += diff1; } } } else if(inline_boxes_dims.count != 0) { // We have inline boxes only m_height = inline_boxes_dims.height(); top_shift = std::abs(inline_boxes_dims.top); m_baseline = inline_boxes_dims.bottom; } else { // We don't have inline items and inline boxes top_shift = 0; } const int top_down_height = std::max(top_aligned_max_height.max_height, bottom_aligned_max_height.max_height); if(top_down_height > m_height) { if(bottom_aligned_max_height.count) { top_shift += bottom_aligned_max_height.height() - m_height; } m_height = top_down_height; } } else { // Add inline boxes dimentions line_max_height.top = std::min(line_max_height.top, inline_boxes_dims.top); line_max_height.bottom = std::max(line_max_height.bottom, inline_boxes_dims.bottom); // Height is maximum from inline elements height and top/bottom aligned elements height m_height = std::max({line_max_height.height(), top_aligned_max_height.height(), bottom_aligned_max_height.height()}); top_shift = -std::min(line_max_height.top, line_max_height.bottom - bottom_aligned_max_height.height()); m_baseline = line_max_height.bottom; } struct inline_item_box { std::shared_ptr element; position box; inline_item_box() = default; explicit inline_item_box(const std::shared_ptr& el) : element(el) {} }; std::list inlines; contexts.clear(); current_context.baseline = 0; current_context.fm = m_font_metrics; current_context.start_lbi = nullptr; //int va_top_bottom = 0; // Second pass: // 1. Vertical align top/bottom // 2. Apply relative shift // 3. Calculate inline boxes for (const auto& lbi : m_items) { if(is_one_of(lbi->get_type(), line_box_item::type_inline_start, line_box_item::type_inline_continue)) { contexts.push_back(current_context); current_context.fm = lbi->get_el()->css().get_font_metrics(); if(lbi->get_el()->css().get_vertical_align() == va_top) { current_context.baseline = m_top - lbi->get_items_top(); current_context.start_lbi = lbi.get(); } else if(lbi->get_el()->css().get_vertical_align() == va_bottom) { current_context.baseline = m_top + m_height - lbi->get_items_bottom(); current_context.start_lbi = lbi.get(); } } else if(lbi->get_type() == line_box_item::type_inline_end) { if(!contexts.empty()) { current_context = contexts.back(); contexts.pop_back(); } } if(current_context.start_lbi) { lbi->pos().y = current_context.baseline - lbi->get_el()->get_last_baseline() + lbi->get_el()->content_offset_top(); } else if(is_one_of(lbi->get_el()->css().get_vertical_align(), va_top, va_bottom) && lbi->get_type() == line_box_item::type_text_part) { if(lbi->get_el()->css().get_vertical_align() == va_top) { lbi->pos().y = m_top + lbi->get_el()->content_offset_top(); } else { lbi->pos().y = m_top + m_height - (lbi->bottom() - lbi->top()) + lbi->get_el()->content_offset_bottom(); } } else { // move element to the correct position lbi->pos().y += m_top + top_shift; } lbi->get_el()->apply_relative_shift(containing_block_size); // Calculate and push inline box into the render item element if(lbi->get_type() == line_box_item::type_inline_start || lbi->get_type() == line_box_item::type_inline_continue) { if(lbi->get_type() == line_box_item::type_inline_start) { lbi->get_el()->clear_inline_boxes(); } inlines.emplace_back(lbi->get_el()); inlines.back().box.x = lbi->left(); inlines.back().box.y = lbi->top() - lbi->get_el()->content_offset_top(); inlines.back().box.height = lbi->bottom() - lbi->top() + lbi->get_el()->content_offset_height(); } else if(lbi->get_type() == line_box_item::type_inline_end) { if(!inlines.empty()) { inlines.back().box.width = lbi->right() - inlines.back().box.x; inlines.back().element->add_inline_box(inlines.back().box); inlines.pop_back(); } } } for(auto iter = inlines.rbegin(); iter != inlines.rend(); ++iter) { iter->box.width = m_items.back()->right() - iter->box.x; iter->element->add_inline_box(iter->box); ret_items.emplace_front(std::unique_ptr(new lbi_continue(iter->element))); } return ret_items; } std::shared_ptr litehtml::line_box::get_first_text_part() const { for(const auto & item : m_items) { if(item->get_type() == line_box_item::type_text_part) { return item->get_el(); } } return nullptr; } std::shared_ptr litehtml::line_box::get_last_text_part() const { for(auto iter = m_items.rbegin(); iter != m_items.rend(); iter++) { if((*iter)->get_type() == line_box_item::type_text_part) { return (*iter)->get_el(); } } return nullptr; } bool litehtml::line_box::can_hold(const std::unique_ptr& item, white_space ws) const { if(!item->get_el()->src_el()->is_inline()) return false; if(item->get_type() == line_box_item::type_text_part) { // force new line on floats clearing if (item->get_el()->src_el()->is_break() && item->get_el()->css().get_clear() != clear_none) { return false; } auto last_el = get_last_text_part(); // the first word is always can be hold if(!last_el) { return true; } // force new line if the last placed element was line break // Skip If the break item is float clearing if (last_el && last_el->src_el()->is_break() && last_el->css().get_clear() == clear_none) { return false; } // line break should stay in current line box if (item->get_el()->src_el()->is_break()) { return true; } if (ws == white_space_nowrap || ws == white_space_pre || (ws == white_space_pre_wrap && item->get_el()->src_el()->is_space())) { return true; } if (m_left + m_width + item->width() > m_right) { return false; } } return true; } bool litehtml::line_box::have_last_space() const { auto last_el = get_last_text_part(); if(last_el) { return last_el->src_el()->is_white_space() || last_el->src_el()->is_break(); } return false; } bool litehtml::line_box::is_empty() const { if(m_items.empty()) return true; if(m_items.size() == 1 && m_items.front()->get_el()->src_el()->is_break() && m_items.front()->get_el()->src_el()->css().get_clear() != clear_none) { return true; } for (const auto& el : m_items) { if(el->get_type() == line_box_item::type_text_part) { if (!el->get_el()->skip() || el->get_el()->src_el()->is_break()) { return false; } } } return true; } int litehtml::line_box::baseline() const { return m_baseline; } int litehtml::line_box::top_margin() const { return 0; } int litehtml::line_box::bottom_margin() const { return 0; } void litehtml::line_box::y_shift( int shift ) { m_top += shift; for (auto& el : m_items) { el->pos().y += shift; } } bool litehtml::line_box::is_break_only() const { if(m_items.empty()) return false; bool break_found = false; for (auto iter = m_items.rbegin(); iter != m_items.rend(); iter++) { if((*iter)->get_type() == line_box_item::type_text_part) { if((*iter)->get_el()->src_el()->is_break()) { break_found = true; } else if(!(*iter)->get_el()->skip()) { return false; } } } return break_found; } std::list< std::unique_ptr > litehtml::line_box::new_width( int left, int right) { std::list< std::unique_ptr > ret_items; int add = left - m_left; if(add) { m_left = left; m_right = right; m_width = 0; auto remove_begin = m_items.end(); auto i = m_items.begin(); i++; while (i != m_items.end()) { if(!(*i)->get_el()->skip()) { if(m_left + m_width + (*i)->width() > m_right) { remove_begin = i; break; } (*i)->pos().x += add; m_width += (*i)->get_el()->width(); } i++; } if(remove_begin != m_items.end()) { while(remove_begin != m_items.end()) { ret_items.emplace_back(std::move(*remove_begin)); } m_items.erase(remove_begin, m_items.end()); } } return ret_items; }