mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 01:37:34 +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:
parent
ea5da0f9b5
commit
eb77e680ed
9 changed files with 72 additions and 25 deletions
|
@ -1,17 +1,16 @@
|
|||
<html>
|
||||
<head><title>Lorem Ipsum</title></head>
|
||||
<style>
|
||||
p { text-align: right; }
|
||||
</style>
|
||||
<body>
|
||||
<h1>Lorem Ipsum</h1>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. In non elit dignissim, lobortis velit id, rutrum enim. Fusce urna nulla, semper in nisl consectetur, dictum dignissim felis. Vivamus mollis porttitor neque non pulvinar. Donec sollicitudin pulvinar nisi, nec vestibulum massa rutrum id. Aenean convallis tincidunt diam vel egestas. Pellentesque laoreet commodo arcu id dignissim. Etiam mattis elementum lectus, ut ultricies nibh dapibus sit amet. Curabitur sodales cursus ipsum vitae porttitor. Vestibulum ac nulla auctor, imperdiet augue accumsan, ornare eros.</p>
|
||||
<p style="text-align: justify">Lorem ipsum dolor sit amet, consectetur adipiscing elit. In non elit dignissim, lobortis velit id, rutrum enim. Fusce urna nulla, semper in nisl consectetur, dictum dignissim felis. Vivamus mollis porttitor neque non pulvinar. Donec sollicitudin pulvinar nisi, nec vestibulum massa rutrum id. Aenean convallis tincidunt diam vel egestas. Pellentesque laoreet commodo arcu id dignissim. Etiam mattis elementum lectus, ut ultricies nibh dapibus sit amet. Curabitur sodales cursus ipsum vitae porttitor. Vestibulum ac nulla auctor, imperdiet augue accumsan, ornare eros.</p>
|
||||
|
||||
<p>Proin vel orci lobortis, ultrices nunc non, placerat odio. Proin nec nibh et odio pellentesque lobortis. Donec id urna ac sapien commodo facilisis in quis magna. Ut tempus aliquet elit, ut semper ante accumsan ornare. Morbi ac egestas quam. Pellentesque ut convallis metus, sit amet dignissim turpis. Sed feugiat hendrerit nibh, id tincidunt tortor euismod et. In vel fringilla ante. Etiam volutpat risus egestas congue sollicitudin.</p>
|
||||
<p style="text-align: right">Proin vel orci lobortis, ultrices nunc non, placerat odio. Proin nec nibh et odio pellentesque lobortis. Donec id urna ac sapien commodo facilisis in quis magna. Ut tempus aliquet elit, ut semper ante accumsan ornare. Morbi ac egestas quam. Pellentesque ut convallis metus, sit amet dignissim turpis. Sed feugiat hendrerit nibh, id tincidunt tortor euismod et. In vel fringilla ante. Etiam volutpat risus egestas congue sollicitudin.</p>
|
||||
|
||||
<p>Sed libero urna, fermentum quis leo at, lacinia suscipit ipsum. Vivamus in dignissim nibh. Proin ultricies sapien quis tortor luctus vehicula. Morbi ut consequat ipsum. Morbi imperdiet lectus libero, at tristique erat scelerisque sed. Duis eu risus at lectus vehicula facilisis. In tempor felis a nulla imperdiet volutpat. Quisque at auctor libero. Nunc ornare eros eget libero faucibus, vehicula ullamcorper erat laoreet. Aliquam dignissim eget est et aliquam. Phasellus imperdiet tincidunt mi, vitae viverra enim elementum a. Nullam pellentesque odio eu mauris bibendum tempor.</p>
|
||||
<p style="text-align: center">Sed libero urna, fermentum quis leo at, lacinia suscipit ipsum. Vivamus in dignissim nibh. Proin ultricies sapien quis tortor luctus vehicula. Morbi ut consequat ipsum. Morbi imperdiet lectus libero, at tristique erat scelerisque sed. Duis eu risus at lectus vehicula facilisis. In tempor felis a nulla imperdiet volutpat. Quisque at auctor libero. Nunc ornare eros eget libero faucibus, vehicula ullamcorper erat laoreet. Aliquam dignissim eget est et aliquam. Phasellus imperdiet tincidunt mi, vitae viverra enim elementum a. Nullam pellentesque odio eu mauris bibendum tempor.</p>
|
||||
|
||||
<p>Sed mattis, elit eu pulvinar sagittis, ipsum enim interdum nisl, eu ornare augue orci at enim. Sed cursus, dolor in vestibulum maximus, mauris magna bibendum enim, in fringilla mauris metus vel nunc. Cras in quam mi. Nullam aliquam velit mauris, quis aliquet nulla pretium auctor. Donec non lobortis tellus. Nunc sodales libero id libero ultricies cursus. Cras ipsum nibh, dictum eu augue fermentum, blandit bibendum odio. Pellentesque tincidunt hendrerit aliquam. Donec sit amet justo vel magna pretium lobortis tempus vitae lorem. Maecenas quam purus, scelerisque dapibus lectus at, mattis tempus enim. Suspendisse ac ante turpis. Suspendisse aliquet, velit at hendrerit elementum, risus tortor accumsan est, quis luctus nisl sapien sed risus. Donec cursus ex diam, nec iaculis urna bibendum eget. Cras neque lacus, ornare eget elit eu, fringilla vestibulum velit. Phasellus lacinia condimentum enim accumsan aliquam. Nulla finibus ex elit, id semper erat posuere suscipit.</p>
|
||||
<p style="text-align: left">Sed mattis, elit eu pulvinar sagittis, ipsum enim interdum nisl, eu ornare augue orci at enim. Sed cursus, dolor in vestibulum maximus, mauris magna bibendum enim, in fringilla mauris metus vel nunc. Cras in quam mi. Nullam aliquam velit mauris, quis aliquet nulla pretium auctor. Donec non lobortis tellus. Nunc sodales libero id libero ultricies cursus. Cras ipsum nibh, dictum eu augue fermentum, blandit bibendum odio. Pellentesque tincidunt hendrerit aliquam. Donec sit amet justo vel magna pretium lobortis tempus vitae lorem. Maecenas quam purus, scelerisque dapibus lectus at, mattis tempus enim. Suspendisse ac ante turpis. Suspendisse aliquet, velit at hendrerit elementum, risus tortor accumsan est, quis luctus nisl sapien sed risus. Donec cursus ex diam, nec iaculis urna bibendum eget. Cras neque lacus, ornare eget elit eu, fringilla vestibulum velit. Phasellus lacinia condimentum enim accumsan aliquam. Nulla finibus ex elit, id semper erat posuere suscipit.</p>
|
||||
|
||||
<p>Integer at libero purus. Maecenas eu cursus nunc, vitae pellentesque sapien. Mauris auctor condimentum massa. Sed pharetra nibh varius leo rutrum, vel auctor tortor venenatis. Donec tincidunt tempus libero vel iaculis. Nam pretium non augue et pretium. Nunc dignissim tortor venenatis, blandit sem ac, mattis dolor. Donec et lacinia nunc. Vestibulum enim eros, aliquam pulvinar cursus ornare, volutpat eu mi. Quisque semper mi id metus elementum malesuada. Suspendisse nisl felis, pretium id consectetur quis, lacinia sit amet est.</p>
|
||||
</body>
|
||||
|
|
|
@ -20,6 +20,7 @@ enum class ValueID {
|
|||
Center,
|
||||
Left,
|
||||
Right,
|
||||
Justify,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
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() };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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 == " ";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue