mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-11 12:06:07 +00:00
LibWeb: Improve graphical list item marker positioning
While 788d5368a7
took care of better text
marker positioning, this improves graphical marker positioning instead.
By looking at how Firefox and Chrome render markers, it's clear that
there are three parts to positioning a graphical marker:
* The containing space that the marker resides in;
* The marker dimensions;
* The distance between the marker and the start of the list item.
The space that the marker can be contained in, is the area to the left
of the list item with a height of the marker's line-height. The marker
dimensions are relative to the marker's font's pixel size: most of them
are a square at 35% of the font size, but the disclosure markers are
sized at 50% instead. Finally, the marker distance is always gauged at
50% of the font size.
So for example, a list item with `list-style-type: disc` and `font-size:
20px`, has 10px between its start and the right side of the marker, and
the marker's dimensions are 7x7.
The percentages I've chosen closely resemble how Firefox lays out its
list item markers.
This commit is contained in:
parent
ada198bee0
commit
115e5f42af
Notes:
github-actions[bot]
2025-07-17 08:36:21 +00:00
Author: https://github.com/gmta
Commit: 115e5f42af
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5475
Reviewed-by: https://github.com/AtkinsSJ ✅
21 changed files with 187 additions and 171 deletions
|
@ -831,9 +831,8 @@ void BlockFormattingContext::layout_block_level_box(Box const& box, BlockContain
|
|||
compute_inset(box, content_box_rect(block_container_state).size());
|
||||
|
||||
// Now that our children are formatted we place the ListItemBox with the left space we remembered.
|
||||
if (is<ListItemBox>(box)) {
|
||||
if (is<ListItemBox>(box))
|
||||
layout_list_item_marker(static_cast<ListItemBox const&>(box), left_space_before_children_formatted);
|
||||
}
|
||||
|
||||
bottom_of_lowest_margin_box = max(bottom_of_lowest_margin_box, box_state.offset.y() + box_state.content_height() + box_state.margin_box_bottom());
|
||||
|
||||
|
@ -1192,24 +1191,26 @@ void BlockFormattingContext::ensure_sizes_correct_for_left_offset_calculation(Li
|
|||
auto& marker = *list_item_box.marker();
|
||||
auto& marker_state = m_state.get_mutable(marker);
|
||||
|
||||
CSSPixels image_width = 0;
|
||||
CSSPixels image_height = 0;
|
||||
// If an image is used, the marker's dimensions are the same as the image.
|
||||
if (auto const* list_style_image = marker.list_style_image()) {
|
||||
image_width = list_style_image->natural_width().value_or(0);
|
||||
image_height = list_style_image->natural_height().value_or(0);
|
||||
marker_state.set_content_width(list_style_image->natural_width().value_or(0));
|
||||
marker_state.set_content_height(list_style_image->natural_height().value_or(0));
|
||||
return;
|
||||
}
|
||||
|
||||
CSSPixels marker_size = marker.relative_size();
|
||||
marker_state.set_content_height(marker_size);
|
||||
|
||||
// Text markers use text metrics to determine their width; other markers use square dimensions.
|
||||
auto const& marker_font = marker.first_available_font();
|
||||
auto marker_text = marker.text();
|
||||
if (!marker_text.has_value()) {
|
||||
auto default_marker_width = max(4, marker.first_available_font().pixel_size() - 4);
|
||||
marker_state.set_content_width(image_width + default_marker_width);
|
||||
} else {
|
||||
if (marker_text.has_value()) {
|
||||
// FIXME: Use per-code-point fonts to measure text.
|
||||
auto text_width = marker.first_available_font().width(marker_text.value().code_points());
|
||||
marker_state.set_content_width(image_width + CSSPixels::nearest_value_for(text_width));
|
||||
auto text_width = marker_font.width(marker_text.value().code_points());
|
||||
marker_state.set_content_width(CSSPixels::nearest_value_for(text_width));
|
||||
} else {
|
||||
marker_state.set_content_width(marker_size);
|
||||
}
|
||||
|
||||
marker_state.set_content_height(max(image_height, CSSPixels { marker.first_available_font().pixel_size() }));
|
||||
}
|
||||
|
||||
void BlockFormattingContext::layout_list_item_marker(ListItemBox const& list_item_box, CSSPixels const& left_space_before_list_item_elements_formatted)
|
||||
|
@ -1222,20 +1223,26 @@ void BlockFormattingContext::layout_list_item_marker(ListItemBox const& list_ite
|
|||
auto& list_item_state = m_state.get_mutable(list_item_box);
|
||||
|
||||
auto marker_text = marker.text();
|
||||
auto default_marker_width = marker_text.has_value() ? 0 : max(4, marker.first_available_font().pixel_size() - 4);
|
||||
auto final_marker_width = marker_state.content_width() + default_marker_width;
|
||||
|
||||
// Text markers fit snug against the list item; non-text position themselves at 50% of the font size.
|
||||
CSSPixels marker_distance = 0;
|
||||
if (!marker_text.has_value())
|
||||
marker_distance = CSSPixels::nearest_value_for(.5f * marker.first_available_font().pixel_size());
|
||||
|
||||
auto marker_height = marker_state.content_height();
|
||||
auto marker_width = marker_state.content_width();
|
||||
|
||||
if (marker.list_style_position() == CSS::ListStylePosition::Inside) {
|
||||
list_item_state.set_content_x(list_item_state.offset.x() + final_marker_width);
|
||||
list_item_state.set_content_width(list_item_state.content_width() - final_marker_width);
|
||||
list_item_state.set_content_x(list_item_state.offset.x() + marker_width + marker_distance);
|
||||
list_item_state.set_content_width(list_item_state.content_width() - marker_width);
|
||||
}
|
||||
|
||||
auto offset_y = max(CSSPixels(0), (marker.computed_values().line_height() - marker_state.content_height()) / 2);
|
||||
auto offset_x = round(left_space_before_list_item_elements_formatted - marker_distance - marker_width);
|
||||
auto offset_y = round(max(CSSPixels(0), (marker.computed_values().line_height() - marker_height) / 2));
|
||||
marker_state.set_content_offset({ offset_x, offset_y });
|
||||
|
||||
marker_state.set_content_offset({ left_space_before_list_item_elements_formatted - final_marker_width, round(offset_y) });
|
||||
|
||||
if (marker_state.content_height() > list_item_state.content_height())
|
||||
list_item_state.set_content_height(marker_state.content_height());
|
||||
if (marker_height > list_item_state.content_height())
|
||||
list_item_state.set_content_height(marker_height);
|
||||
}
|
||||
|
||||
BlockFormattingContext::SpaceUsedAndContainingMarginForFloats BlockFormattingContext::space_used_and_containing_margin_for_floats(CSSPixels y) const
|
||||
|
|
|
@ -73,6 +73,23 @@ GC::Ptr<Painting::Paintable> ListItemMarkerBox::create_paintable() const
|
|||
return Painting::MarkerPaintable::create(*this);
|
||||
}
|
||||
|
||||
CSSPixels ListItemMarkerBox::relative_size() const
|
||||
{
|
||||
auto font_size = first_available_font().pixel_size();
|
||||
auto marker_text = text();
|
||||
if (marker_text.has_value())
|
||||
return CSSPixels::nearest_value_for(font_size);
|
||||
|
||||
// Scale the marker box relative to the used font's pixel size.
|
||||
switch (m_list_style_type.get<CSS::CounterStyleNameKeyword>()) {
|
||||
case CSS::CounterStyleNameKeyword::DisclosureClosed:
|
||||
case CSS::CounterStyleNameKeyword::DisclosureOpen:
|
||||
return CSSPixels::nearest_value_for(ceilf(font_size * .5f));
|
||||
default:
|
||||
return CSSPixels::nearest_value_for(ceilf(font_size * .35f));
|
||||
}
|
||||
}
|
||||
|
||||
void ListItemMarkerBox::visit_edges(Cell::Visitor& visitor)
|
||||
{
|
||||
Base::visit_edges(visitor);
|
||||
|
|
|
@ -27,6 +27,8 @@ public:
|
|||
CSS::ListStyleType const& list_style_type() const { return m_list_style_type; }
|
||||
CSS::ListStylePosition list_style_position() const { return m_list_style_position; }
|
||||
|
||||
CSSPixels relative_size() const;
|
||||
|
||||
private:
|
||||
virtual void visit_edges(Cell::Visitor&) override;
|
||||
|
||||
|
|
|
@ -40,55 +40,43 @@ void MarkerPaintable::paint(PaintContext& context, PaintPhase phase) const
|
|||
if (phase != PaintPhase::Foreground)
|
||||
return;
|
||||
|
||||
CSSPixelRect enclosing = absolute_rect().to_rounded<CSSPixels>();
|
||||
auto device_enclosing = context.enclosing_device_rect(enclosing);
|
||||
|
||||
CSSPixels marker_width = enclosing.height() / 2;
|
||||
auto marker_rect = absolute_rect().to_rounded<CSSPixels>();
|
||||
auto device_rect = context.enclosing_device_rect(marker_rect);
|
||||
|
||||
if (auto const* list_style_image = layout_box().list_style_image()) {
|
||||
CSSPixelRect image_rect {
|
||||
0, 0,
|
||||
list_style_image->natural_width().value_or(marker_width),
|
||||
list_style_image->natural_height().value_or(marker_width)
|
||||
};
|
||||
image_rect.center_within(enclosing);
|
||||
|
||||
auto device_image_rect = context.enclosing_device_rect(image_rect);
|
||||
list_style_image->resolve_for_size(layout_box(), image_rect.size());
|
||||
list_style_image->paint(context, device_image_rect, computed_values().image_rendering());
|
||||
list_style_image->resolve_for_size(layout_box(), marker_rect.size());
|
||||
list_style_image->paint(context, device_rect, computed_values().image_rendering());
|
||||
return;
|
||||
}
|
||||
|
||||
CSSPixelRect marker_rect { 0, 0, marker_width, marker_width };
|
||||
marker_rect.center_within(enclosing);
|
||||
auto device_marker_rect = context.enclosing_device_rect(marker_rect);
|
||||
|
||||
float left = device_marker_rect.x().value();
|
||||
float right = left + device_marker_rect.width().value();
|
||||
float top = device_marker_rect.y().value();
|
||||
float bottom = top + device_marker_rect.height().value();
|
||||
float left = device_rect.x().value();
|
||||
float right = left + device_rect.width().value();
|
||||
float top = device_rect.y().value();
|
||||
float bottom = top + device_rect.height().value();
|
||||
|
||||
auto color = computed_values().color();
|
||||
|
||||
if (auto text = layout_box().text(); text.has_value()) {
|
||||
// FIXME: This should use proper text layout logic!
|
||||
// This does not line up with the text in the <li> element which looks very sad :(
|
||||
context.display_list_recorder().draw_text(device_enclosing.to_type<int>(), *text, layout_box().font(context), Gfx::TextAlignment::Center, color);
|
||||
context.display_list_recorder().draw_text(device_rect.to_type<int>(), *text, layout_box().font(context), Gfx::TextAlignment::Center, color);
|
||||
} else if (auto const* counter_style = layout_box().list_style_type().get_pointer<CSS::CounterStyleNameKeyword>()) {
|
||||
switch (*counter_style) {
|
||||
case CSS::CounterStyleNameKeyword::Square:
|
||||
context.display_list_recorder().fill_rect(device_marker_rect.to_type<int>(), color);
|
||||
context.display_list_recorder().fill_rect(device_rect.to_type<int>(), color);
|
||||
break;
|
||||
case CSS::CounterStyleNameKeyword::Circle:
|
||||
context.display_list_recorder().draw_ellipse(device_marker_rect.to_type<int>(), color, 1);
|
||||
context.display_list_recorder().draw_ellipse(device_rect.to_type<int>(), color, 1);
|
||||
break;
|
||||
case CSS::CounterStyleNameKeyword::Disc:
|
||||
context.display_list_recorder().fill_ellipse(device_marker_rect.to_type<int>(), color);
|
||||
context.display_list_recorder().fill_ellipse(device_rect.to_type<int>(), color);
|
||||
break;
|
||||
case CSS::CounterStyleNameKeyword::DisclosureClosed: {
|
||||
// https://drafts.csswg.org/css-counter-styles-3/#disclosure-closed
|
||||
// For the disclosure-open and disclosure-closed counter styles, the marker must be an image or character suitable for indicating the open and closed states of a disclosure widget, such as HTML’s details element.
|
||||
// FIXME: If the image is directional, it must respond to the writing mode of the element, similar to the bidi-sensitive images feature of the Images 4 module.
|
||||
// For the disclosure-open and disclosure-closed counter styles, the marker must be an image or character
|
||||
// suitable for indicating the open and closed states of a disclosure widget, such as HTML’s details element.
|
||||
// FIXME: If the image is directional, it must respond to the writing mode of the element, similar to the
|
||||
// bidi-sensitive images feature of the Images 4 module.
|
||||
|
||||
// Draw an equilateral triangle pointing right.
|
||||
auto path = Gfx::Path();
|
||||
|
@ -101,8 +89,10 @@ void MarkerPaintable::paint(PaintContext& context, PaintPhase phase) const
|
|||
}
|
||||
case CSS::CounterStyleNameKeyword::DisclosureOpen: {
|
||||
// https://drafts.csswg.org/css-counter-styles-3/#disclosure-open
|
||||
// For the disclosure-open and disclosure-closed counter styles, the marker must be an image or character suitable for indicating the open and closed states of a disclosure widget, such as HTML’s details element.
|
||||
// FIXME: If the image is directional, it must respond to the writing mode of the element, similar to the bidi-sensitive images feature of the Images 4 module.
|
||||
// For the disclosure-open and disclosure-closed counter styles, the marker must be an image or character
|
||||
// suitable for indicating the open and closed states of a disclosure widget, such as HTML’s details element.
|
||||
// FIXME: If the image is directional, it must respond to the writing mode of the element, similar to the
|
||||
// bidi-sensitive images feature of the Images 4 module.
|
||||
|
||||
// Draw an equilateral triangle pointing down.
|
||||
auto path = Gfx::Path();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue