1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 22:17:45 +00:00

LibHTML: Implement "text-align: justify"

In order for this to work nicely, I made the line box classes use float
instead of int for its geometry information.

Justification works by distributing all of the whitespace on the line
(including the trailing whitespace before the line break) evenly across
the spaces in-between words.

We should probably use floating point (or maybe fixed point?) for all
the layout metrics stuff. But one thing at a time. :^)
This commit is contained in:
Andreas Kling 2019-10-20 12:30:25 +02:00
parent ea5da0f9b5
commit eb77e680ed
9 changed files with 72 additions and 25 deletions

View file

@ -4,6 +4,8 @@
#include <LibHTML/Layout/LayoutBlock.h>
#include <LibHTML/Layout/LayoutInline.h>
#include <LibHTML/Layout/LayoutReplaced.h>
#include <LibHTML/Layout/LayoutText.h>
#include <math.h>
LayoutBlock::LayoutBlock(const Node* node, NonnullRefPtr<StyleProperties> style)
: LayoutBox(node, move(style))
@ -72,34 +74,66 @@ void LayoutBlock::layout_inline_children()
text_align = CSS::ValueID::Left;
else if (text_align_string == "right")
text_align = CSS::ValueID::Right;
else if (text_align_string == "justify")
text_align = CSS::ValueID::Justify;
for (auto& line_box : m_line_boxes) {
int max_height = min_line_height;
for (auto& fragment : line_box.fragments()) {
max_height = max(max_height, fragment.rect().height());
max_height = max(max_height, enclosing_int_rect(fragment.rect()).height());
}
int x_offset = x();
int excess_horizontal_space = width() - line_box.width();
switch (text_align) {
case CSS::ValueID::Center:
x_offset += (width() - line_box.width()) / 2;
x_offset += excess_horizontal_space / 2;
break;
case CSS::ValueID::Right:
x_offset += (width() - line_box.width());
x_offset += excess_horizontal_space;
break;
case CSS::ValueID::Left:
case CSS::ValueID::Justify:
default:
break;
}
for (auto& fragment : line_box.fragments()) {
int excess_horizontal_space_including_whitespace = excess_horizontal_space;
int whitespace_count = 0;
if (text_align == CSS::ValueID::Justify) {
for (auto& fragment : line_box.fragments()) {
if (fragment.is_justifiable_whitespace()) {
++whitespace_count;
excess_horizontal_space_including_whitespace += fragment.rect().width();
}
}
}
float justified_space_width = whitespace_count ? ((float)excess_horizontal_space_including_whitespace / (float)whitespace_count) : 0;
for (int i = 0; i < line_box.fragments().size(); ++i) {
auto& fragment = line_box.fragments()[i];
// Vertically align everyone's bottom to the line.
// FIXME: Support other kinds of vertical alignment.
fragment.rect().set_x(x_offset + fragment.rect().x());
fragment.rect().set_y(y() + content_height + (max_height - fragment.rect().height()));
if (text_align == CSS::ValueID::Justify) {
if (fragment.is_justifiable_whitespace()) {
if (fragment.rect().width() != justified_space_width) {
float diff = justified_space_width - fragment.rect().width();
fragment.rect().set_width(justified_space_width);
// Shift subsequent sibling fragments to the right to adjust for change in width.
for (int j = i + 1; j < line_box.fragments().size(); ++j) {
line_box.fragments()[j].rect().move_by(diff, 0);
}
}
}
}
if (is<LayoutReplaced>(fragment.layout_node()))
const_cast<LayoutReplaced&>(to<LayoutReplaced>(fragment.layout_node())).set_rect(fragment.rect());
const_cast<LayoutReplaced&>(to<LayoutReplaced>(fragment.layout_node())).set_rect(enclosing_int_rect(fragment.rect()));
}
content_height += max_height;
@ -234,7 +268,7 @@ void LayoutBlock::render(RenderingContext& context)
for (auto& line_box : m_line_boxes) {
for (auto& fragment : line_box.fragments()) {
if (context.should_show_line_box_borders())
context.painter().draw_rect(fragment.rect(), Color::Green);
context.painter().draw_rect(enclosing_int_rect(fragment.rect()), Color::Green);
fragment.render(context);
}
}
@ -249,7 +283,7 @@ HitTestResult LayoutBlock::hit_test(const Point& position) const
HitTestResult result;
for (auto& line_box : m_line_boxes) {
for (auto& fragment : line_box.fragments()) {
if (fragment.rect().contains(position)) {
if (enclosing_int_rect(fragment.rect()).contains(position)) {
return { fragment.layout_node() };
}
}

View file

@ -82,7 +82,7 @@ void LayoutNode::set_needs_display()
if (auto* block = containing_block()) {
block->for_each_fragment([&](auto& fragment) {
if (&fragment.layout_node() == this || is_ancestor_of(fragment.layout_node())) {
const_cast<Frame*>(frame)->set_needs_display(fragment.rect());
const_cast<Frame*>(frame)->set_needs_display(enclosing_int_rect(fragment.rect()));
}
return IterationDecision::Continue;
});
@ -98,7 +98,7 @@ Point LayoutNode::box_type_agnostic_position() const
if (auto* block = containing_block()) {
block->for_each_fragment([&](auto& fragment) {
if (&fragment.layout_node() == this || is_ancestor_of(fragment.layout_node())) {
position = fragment.rect().location();
position = enclosing_int_rect(fragment.rect()).location();
return IterationDecision::Break;
}
return IterationDecision::Continue;

View file

@ -46,9 +46,9 @@ void LayoutText::render_fragment(RenderingContext& context, const LineBoxFragmen
bool is_underline = text_decoration == "underline";
if (is_underline)
painter.draw_line(fragment.rect().bottom_left().translated(0, -1), fragment.rect().bottom_right().translated(0, -1), color);
painter.draw_line(enclosing_int_rect(fragment.rect()).bottom_left().translated(0, -1), enclosing_int_rect(fragment.rect()).bottom_right().translated(0, -1), color);
painter.draw_text(fragment.rect(), m_text_for_rendering.substring_view(fragment.start(), fragment.length()), TextAlignment::TopLeft, color);
painter.draw_text(enclosing_int_rect(fragment.rect()), m_text_for_rendering.substring_view(fragment.start(), fragment.length()), TextAlignment::TopLeft, color);
}
template<typename Callback>

View file

@ -1,14 +1,16 @@
#include <LibHTML/Layout/LayoutNode.h>
#include <LibHTML/Layout/LineBox.h>
void LineBox::add_fragment(const LayoutNode& layout_node, int start, int length, int width, int height)
{
if (!m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node) {
bool text_align_is_justify = layout_node.style().string_or_fallback(CSS::PropertyID::TextAlign, "left") == "justify";
if (!text_align_is_justify && !m_fragments.is_empty() && &m_fragments.last().layout_node() == &layout_node) {
// The fragment we're adding is from the last LayoutNode on the line.
// Expand the last fragment instead of adding a new one with the same LayoutNode.
m_fragments.last().m_length = (start - m_fragments.last().m_start) + length;
m_fragments.last().m_rect.set_width(m_fragments.last().m_rect.width() + width);
} else {
m_fragments.empend(layout_node, start, length, Rect(m_width, 0, width, height));
m_fragments.empend(layout_node, start, length, FloatRect(m_width, 0, width, height));
}
m_width += width;
}

View file

@ -7,7 +7,7 @@ class LineBox {
public:
LineBox() {}
int width() const { return m_width; }
float width() const { return m_width; }
void add_fragment(const LayoutNode& layout_node, int start, int length, int width, int height);
@ -16,5 +16,5 @@ public:
private:
Vector<LineBoxFragment> m_fragments;
int m_width { 0 };
float m_width { 0 };
};

View file

@ -14,3 +14,12 @@ void LineBoxFragment::render(RenderingContext& context)
to<LayoutText>(layout_node()).render_fragment(context, *this);
}
}
bool LineBoxFragment::is_justifiable_whitespace() const
{
if (!is<LayoutText>(layout_node()))
return false;
auto& layout_text = to<LayoutText>(layout_node());
auto text = layout_text.node().data().substring_view(m_start, m_length);
return text == " ";
}

View file

@ -1,6 +1,6 @@
#pragma once
#include <LibDraw/Rect.h>
#include <LibDraw/FloatRect.h>
class LayoutNode;
class RenderingContext;
@ -8,7 +8,7 @@ class RenderingContext;
class LineBoxFragment {
friend class LineBox;
public:
LineBoxFragment(const LayoutNode& layout_node, int start, int length, const Rect& rect)
LineBoxFragment(const LayoutNode& layout_node, int start, int length, const FloatRect& rect)
: m_layout_node(layout_node)
, m_start(start)
, m_length(length)
@ -19,14 +19,16 @@ public:
const LayoutNode& layout_node() const { return m_layout_node; }
int start() const { return m_start; }
int length() const { return m_length; }
const Rect& rect() const { return m_rect; }
Rect& rect() { return m_rect; }
const FloatRect& rect() const { return m_rect; }
FloatRect& rect() { return m_rect; }
void render(RenderingContext&);
bool is_justifiable_whitespace() const;
private:
const LayoutNode& m_layout_node;
int m_start { 0 };
int m_length { 0 };
Rect m_rect;
FloatRect m_rect;
};