use app_units::Au;
use bitflags::bitflags;
use fonts::{FontMetrics, GlyphStore};
use itertools::Either;
use servo_arc::Arc;
use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse;
use style::properties::ComputedValues;
use style::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword};
use style::values::generics::font::LineHeight;
use style::values::specified::align::AlignFlags;
use style::values::specified::box_::DisplayOutside;
use style::values::specified::text::TextDecorationLine;
use style::Zero;
use unicode_bidi::{BidiInfo, Level};
use webrender_api::FontInstanceKey;
use super::inline_box::{InlineBoxContainerState, InlineBoxIdentifier, InlineBoxTreePathToken};
use super::{InlineFormattingContextLayout, LineBlockSizes};
use crate::cell::ArcRefCell;
use crate::fragment_tree::{
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, Fragment, TextFragment,
};
use crate::geom::{AuOrAuto, LogicalRect, LogicalVec2, PhysicalRect, ToLogical};
use crate::positioned::{
relative_adjustement, AbsolutelyPositionedBox, PositioningContext, PositioningContextLength,
};
use crate::ContainingBlock;
pub(super) struct LineMetrics {
pub block_offset: Au,
pub block_size: Au,
pub baseline_block_offset: Au,
}
bitflags! {
struct LineLayoutInlineContainerFlags: u8 {
const HAD_ANY_LINE_ITEMS = 1 << 0;
const HAD_LEFT_PBM = 1 << 2;
const HAD_RIGHT_PBM = 1 << 3;
const HAD_ANY_FLOATS = 1 << 4;
}
}
pub(super) struct LineItemLayoutInlineContainerState {
pub identifier: Option<InlineBoxIdentifier>,
pub fragments: Vec<(Fragment, LogicalRect<Au>)>,
pub inline_advance: Au,
flags: LineLayoutInlineContainerFlags,
pub parent_offset: LogicalVec2<Au>,
pub baseline_offset: Au,
pub positioning_context_or_start_offset_in_parent:
Either<PositioningContext, PositioningContextLength>,
}
impl LineItemLayoutInlineContainerState {
fn new(
identifier: Option<InlineBoxIdentifier>,
parent_offset: LogicalVec2<Au>,
baseline_offset: Au,
positioning_context_or_start_offset_in_parent: Either<
PositioningContext,
PositioningContextLength,
>,
) -> Self {
Self {
identifier,
fragments: Vec::new(),
inline_advance: Au::zero(),
flags: LineLayoutInlineContainerFlags::empty(),
parent_offset,
baseline_offset,
positioning_context_or_start_offset_in_parent,
}
}
fn root(starting_inline_advance: Au, baseline_offset: Au) -> Self {
let mut state = Self::new(
None,
LogicalVec2::zero(),
baseline_offset,
Either::Right(PositioningContextLength::zero()),
);
state.inline_advance = starting_inline_advance;
state
}
}
pub(super) struct LineItemLayout<'layout_data, 'layout> {
layout: &'layout mut InlineFormattingContextLayout<'layout_data>,
pub state_stack: Vec<LineItemLayoutInlineContainerState>,
pub current_state: LineItemLayoutInlineContainerState,
pub line_metrics: LineMetrics,
pub justification_adjustment: Au,
}
impl<'layout_data, 'layout> LineItemLayout<'layout_data, 'layout> {
pub(super) fn layout_line_items(
layout: &mut InlineFormattingContextLayout,
line_items: Vec<LineItem>,
start_position: LogicalVec2<Au>,
effective_block_advance: &LineBlockSizes,
justification_adjustment: Au,
) -> Vec<Fragment> {
let baseline_offset = effective_block_advance.find_baseline_offset();
LineItemLayout {
layout,
state_stack: Vec::new(),
current_state: LineItemLayoutInlineContainerState::root(
start_position.inline,
baseline_offset,
),
line_metrics: LineMetrics {
block_offset: start_position.block,
block_size: effective_block_advance.resolve(),
baseline_block_offset: baseline_offset,
},
justification_adjustment,
}
.layout(line_items)
}
fn prepare_layout_for_inline_box(&mut self, new_inline_box: Option<InlineBoxIdentifier>) {
let Some(new_inline_box) = new_inline_box else {
while !self.state_stack.is_empty() {
self.end_inline_box();
}
return;
};
let path = self
.layout
.ifc
.inline_boxes
.get_path(self.current_state.identifier, new_inline_box);
for token in path {
match token {
InlineBoxTreePathToken::Start(ref identifier) => self.start_inline_box(identifier),
InlineBoxTreePathToken::End(_) => self.end_inline_box(),
}
}
}
pub(super) fn layout(&mut self, mut line_items: Vec<LineItem>) -> Vec<Fragment> {
let mut last_level = Level::ltr();
let levels: Vec<_> = line_items
.iter()
.map(|item| {
let level = match item {
LineItem::TextRun(_, text_run) => text_run.bidi_level,
LineItem::LeftInlineBoxPaddingBorderMargin(_) => last_level,
LineItem::RightInlineBoxPaddingBorderMargin(_) => last_level,
LineItem::Atomic(_, atomic) => atomic.bidi_level,
LineItem::AbsolutelyPositioned(..) => last_level,
LineItem::Float(..) => {
last_level
},
};
last_level = level;
level
})
.collect();
if self.layout.ifc.has_right_to_left_content {
sort_by_indices_in_place(&mut line_items, BidiInfo::reorder_visual(&levels));
}
let line_item_iterator = if self
.layout
.containing_block
.style
.writing_mode
.is_bidi_ltr()
{
Either::Left(line_items.into_iter())
} else {
Either::Right(line_items.into_iter().rev())
};
for item in line_item_iterator.into_iter().by_ref() {
self.prepare_layout_for_inline_box(item.inline_box_identifier());
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_ANY_LINE_ITEMS);
match item {
LineItem::LeftInlineBoxPaddingBorderMargin(_) => {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_LEFT_PBM);
},
LineItem::RightInlineBoxPaddingBorderMargin(_) => {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_RIGHT_PBM);
},
LineItem::TextRun(_, text_run) => self.layout_text_run(text_run),
LineItem::Atomic(_, atomic) => self.layout_atomic(atomic),
LineItem::AbsolutelyPositioned(_, absolute) => self.layout_absolute(absolute),
LineItem::Float(_, float) => self.layout_float(float),
}
}
self.prepare_layout_for_inline_box(None);
let fragments_and_rectangles = std::mem::take(&mut self.current_state.fragments);
fragments_and_rectangles
.into_iter()
.map(|(mut fragment, logical_rect)| {
if matches!(fragment, Fragment::Float(_)) {
return fragment;
}
if let Some(content_rect) = fragment.content_rect_mut() {
*content_rect = logical_rect.to_physical(Some(self.layout.containing_block))
}
fragment
})
.collect()
}
fn current_positioning_context_mut(&mut self) -> &mut PositioningContext {
if let Either::Left(ref mut positioning_context) = self
.current_state
.positioning_context_or_start_offset_in_parent
{
return positioning_context;
}
self.state_stack
.iter_mut()
.rev()
.find_map(
|state| match state.positioning_context_or_start_offset_in_parent {
Either::Left(ref mut positioning_context) => Some(positioning_context),
Either::Right(_) => None,
},
)
.unwrap_or(self.layout.positioning_context)
}
fn start_inline_box(&mut self, identifier: &InlineBoxIdentifier) {
let inline_box_state =
&*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize];
let inline_box = self.layout.ifc.inline_boxes.get(identifier);
let inline_box = &*(inline_box.borrow());
let style = &inline_box.style;
let space_above_baseline = inline_box_state.calculate_space_above_baseline();
let block_start_offset =
self.calculate_inline_box_block_start(inline_box_state, space_above_baseline);
let positioning_context_or_start_offset_in_parent =
match PositioningContext::new_for_style(style) {
Some(positioning_context) => Either::Left(positioning_context),
None => Either::Right(self.current_positioning_context_mut().len()),
};
let parent_offset = LogicalVec2 {
inline: self.current_state.inline_advance + self.current_state.parent_offset.inline,
block: block_start_offset,
};
let outer_state = std::mem::replace(
&mut self.current_state,
LineItemLayoutInlineContainerState::new(
Some(*identifier),
parent_offset,
block_start_offset + space_above_baseline,
positioning_context_or_start_offset_in_parent,
),
);
self.state_stack.push(outer_state);
}
fn end_inline_box(&mut self) {
let outer_state = self.state_stack.pop().expect("Ended unknown inline box");
let inner_state = std::mem::replace(&mut self.current_state, outer_state);
let identifier = inner_state.identifier.expect("Ended unknown inline box");
let inline_box_state =
&*self.layout.inline_box_states[identifier.index_in_inline_boxes as usize];
let inline_box = self.layout.ifc.inline_boxes.get(&identifier);
let inline_box = &*(inline_box.borrow());
let mut padding = inline_box_state.pbm.padding;
let mut border = inline_box_state.pbm.border;
let mut margin = inline_box_state.pbm.margin.auto_is(Au::zero);
let had_left = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_LEFT_PBM);
let had_right = inner_state
.flags
.contains(LineLayoutInlineContainerFlags::HAD_RIGHT_PBM);
let (had_start, had_end) = if self
.layout
.containing_block
.style
.writing_mode
.is_bidi_ltr()
{
(had_left, had_right)
} else {
(had_right, had_left)
};
if !had_start {
padding.inline_start = Au::zero();
border.inline_start = Au::zero();
margin.inline_start = Au::zero();
}
if !had_end {
padding.inline_end = Au::zero();
border.inline_end = Au::zero();
margin.inline_end = Au::zero();
}
let pbm_sums = padding + border + margin;
if inner_state.fragments.is_empty() && !had_start && pbm_sums.inline_sum().is_zero() {
return;
}
let mut content_rect = LogicalRect {
start_corner: LogicalVec2 {
inline: self.current_state.inline_advance + pbm_sums.inline_start,
block: inner_state.parent_offset.block - self.current_state.parent_offset.block,
},
size: LogicalVec2 {
inline: inner_state.inline_advance,
block: inline_box_state.base.font_metrics.line_gap,
},
};
let style = &inline_box.style;
if style.clone_position().is_relative() {
content_rect.start_corner += relative_adjustement(style, self.layout.containing_block);
}
let ifc_writing_mode = self.layout.containing_block.style.writing_mode;
let inline_box_containing_block = ContainingBlock {
inline_size: content_rect.size.inline,
block_size: AuOrAuto::Auto,
style: self.layout.containing_block.style,
};
let fragments = inner_state
.fragments
.into_iter()
.map(|(mut fragment, logical_rect)| {
let is_float = matches!(fragment, Fragment::Float(_));
if let Some(content_rect) = fragment.content_rect_mut() {
if is_float {
content_rect.origin -=
pbm_sums.start_offset().to_physical_size(ifc_writing_mode);
} else {
*content_rect = logical_rect.to_physical(Some(&inline_box_containing_block))
}
}
fragment
})
.collect();
let physical_content_rect = content_rect.to_physical(Some(self.layout.containing_block));
let mut fragment = BoxFragment::new(
inline_box.base_fragment_info,
style.clone(),
fragments,
physical_content_rect,
padding.to_physical(ifc_writing_mode),
border.to_physical(ifc_writing_mode),
margin.to_physical(ifc_writing_mode),
None, CollapsedBlockMargins::zero(),
);
let offset_from_parent_ifc = LogicalVec2 {
inline: pbm_sums.inline_start + self.current_state.inline_advance,
block: content_rect.start_corner.block,
}
.to_physical_vector(self.layout.containing_block.style.writing_mode);
match inner_state.positioning_context_or_start_offset_in_parent {
Either::Left(mut positioning_context) => {
positioning_context
.layout_collected_children(self.layout.layout_context, &mut fragment);
positioning_context.adjust_static_position_of_hoisted_fragments_with_offset(
&offset_from_parent_ifc,
PositioningContextLength::zero(),
);
self.current_positioning_context_mut()
.append(positioning_context);
},
Either::Right(start_offset) => {
self.current_positioning_context_mut()
.adjust_static_position_of_hoisted_fragments_with_offset(
&offset_from_parent_ifc,
start_offset,
);
},
}
self.current_state.inline_advance += inner_state.inline_advance + pbm_sums.inline_sum();
self.current_state
.fragments
.push((Fragment::Box(fragment), content_rect));
}
fn calculate_inline_box_block_start(
&self,
inline_box_state: &InlineBoxContainerState,
space_above_baseline: Au,
) -> Au {
let font_metrics = &inline_box_state.base.font_metrics;
let style = &inline_box_state.base.style;
let line_gap = font_metrics.line_gap;
match inline_box_state.base.style.clone_vertical_align() {
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => {
let line_height: Au = line_height(style, font_metrics);
(line_height - line_gap).scale_by(0.5)
},
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => {
let line_height: Au = line_height(style, font_metrics);
let half_leading = (line_height - line_gap).scale_by(0.5);
self.line_metrics.block_size - line_height + half_leading
},
_ => {
self.line_metrics.baseline_block_offset + inline_box_state.base.baseline_offset -
space_above_baseline
},
}
}
fn layout_text_run(&mut self, text_item: TextRunLineItem) {
if text_item.text.is_empty() {
return;
}
let mut number_of_justification_opportunities = 0;
let mut inline_advance = text_item
.text
.iter()
.map(|glyph_store| {
number_of_justification_opportunities += glyph_store.total_word_separators();
glyph_store.total_advance()
})
.sum();
if !self.justification_adjustment.is_zero() {
inline_advance += self
.justification_adjustment
.scale_by(number_of_justification_opportunities as f32);
}
let start_corner = LogicalVec2 {
inline: self.current_state.inline_advance,
block: self.current_state.baseline_offset -
text_item.font_metrics.ascent -
self.current_state.parent_offset.block,
};
let content_rect = LogicalRect {
start_corner,
size: LogicalVec2 {
block: text_item.font_metrics.line_gap,
inline: inline_advance,
},
};
self.current_state.inline_advance += inline_advance;
self.current_state.fragments.push((
Fragment::Text(TextFragment {
base: text_item.base_fragment_info.into(),
parent_style: text_item.parent_style,
rect: PhysicalRect::zero(),
font_metrics: text_item.font_metrics,
font_key: text_item.font_key,
glyphs: text_item.text,
text_decoration_line: text_item.text_decoration_line,
justification_adjustment: self.justification_adjustment,
}),
content_rect,
));
}
fn layout_atomic(&mut self, atomic: AtomicLineItem) {
let ifc_writing_mode = self.layout.containing_block.style.writing_mode;
let padding_border_margin_sides = atomic
.fragment
.padding_border_margin()
.to_logical(ifc_writing_mode);
let mut atomic_offset = LogicalVec2 {
inline: self.current_state.inline_advance + padding_border_margin_sides.inline_start,
block: atomic.calculate_block_start(&self.line_metrics) -
self.current_state.parent_offset.block +
padding_border_margin_sides.block_start,
};
if atomic.fragment.style.clone_position().is_relative() {
atomic_offset +=
relative_adjustement(&atomic.fragment.style, self.layout.containing_block);
}
let content_rect = LogicalRect {
start_corner: atomic_offset,
size: atomic
.fragment
.content_rect
.size
.to_logical(ifc_writing_mode),
};
if let Some(mut positioning_context) = atomic.positioning_context {
let physical_rect_as_if_in_root =
content_rect.to_physical(Some(self.layout.containing_block));
positioning_context.adjust_static_position_of_hoisted_fragments_with_offset(
&physical_rect_as_if_in_root.origin.to_vector(),
PositioningContextLength::zero(),
);
self.current_positioning_context_mut()
.append(positioning_context);
}
self.current_state.inline_advance += atomic.size.inline;
self.current_state
.fragments
.push((Fragment::Box(atomic.fragment), content_rect));
}
fn layout_absolute(&mut self, absolute: AbsolutelyPositionedLineItem) {
let absolutely_positioned_box = (*absolute.absolutely_positioned_box).borrow();
let style = absolutely_positioned_box.context.style();
let initial_start_corner =
if style.get_box().original_display.outside() == DisplayOutside::Inline {
LogicalVec2 {
inline: self.current_state.inline_advance,
block: -self.current_state.parent_offset.block,
}
} else {
LogicalVec2 {
inline: -self.current_state.parent_offset.inline,
block: self.line_metrics.block_size - self.current_state.parent_offset.block,
}
};
let static_position_rect = LogicalRect {
start_corner: initial_start_corner,
size: LogicalVec2::zero(),
}
.to_physical(Some(self.layout.containing_block));
let hoisted_box = AbsolutelyPositionedBox::to_hoisted(
absolute.absolutely_positioned_box.clone(),
static_position_rect,
LogicalVec2 {
inline: AlignFlags::START,
block: AlignFlags::START,
},
self.layout.containing_block.style.writing_mode,
);
let hoisted_fragment = hoisted_box.fragment.clone();
self.current_positioning_context_mut().push(hoisted_box);
self.current_state.fragments.push((
Fragment::AbsoluteOrFixedPositioned(hoisted_fragment),
LogicalRect::zero(),
));
}
fn layout_float(&mut self, mut float: FloatLineItem) {
self.current_state
.flags
.insert(LineLayoutInlineContainerFlags::HAD_ANY_FLOATS);
let distance_from_parent_to_ifc = LogicalVec2 {
inline: self.current_state.parent_offset.inline,
block: self.line_metrics.block_offset + self.current_state.parent_offset.block,
};
float.fragment.content_rect.origin -= distance_from_parent_to_ifc
.to_physical_size(self.layout.containing_block.style.writing_mode);
self.current_state
.fragments
.push((Fragment::Float(float.fragment), LogicalRect::zero()));
}
}
pub(super) enum LineItem {
LeftInlineBoxPaddingBorderMargin(InlineBoxIdentifier),
RightInlineBoxPaddingBorderMargin(InlineBoxIdentifier),
TextRun(Option<InlineBoxIdentifier>, TextRunLineItem),
Atomic(Option<InlineBoxIdentifier>, AtomicLineItem),
AbsolutelyPositioned(Option<InlineBoxIdentifier>, AbsolutelyPositionedLineItem),
Float(Option<InlineBoxIdentifier>, FloatLineItem),
}
impl LineItem {
fn inline_box_identifier(&self) -> Option<InlineBoxIdentifier> {
match self {
LineItem::LeftInlineBoxPaddingBorderMargin(identifier) => Some(*identifier),
LineItem::RightInlineBoxPaddingBorderMargin(identifier) => Some(*identifier),
LineItem::TextRun(identifier, _) => *identifier,
LineItem::Atomic(identifier, _) => *identifier,
LineItem::AbsolutelyPositioned(identifier, _) => *identifier,
LineItem::Float(identifier, _) => *identifier,
}
}
pub(super) fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool {
match self {
LineItem::LeftInlineBoxPaddingBorderMargin(_) => true,
LineItem::RightInlineBoxPaddingBorderMargin(_) => true,
LineItem::TextRun(_, ref mut item) => item.trim_whitespace_at_end(whitespace_trimmed),
LineItem::Atomic(..) => false,
LineItem::AbsolutelyPositioned(..) => true,
LineItem::Float(..) => true,
}
}
pub(super) fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool {
match self {
LineItem::LeftInlineBoxPaddingBorderMargin(_) => true,
LineItem::RightInlineBoxPaddingBorderMargin(_) => true,
LineItem::TextRun(_, ref mut item) => item.trim_whitespace_at_start(whitespace_trimmed),
LineItem::Atomic(..) => false,
LineItem::AbsolutelyPositioned(..) => true,
LineItem::Float(..) => true,
}
}
}
pub(super) struct TextRunLineItem {
pub base_fragment_info: BaseFragmentInfo,
pub parent_style: Arc<ComputedValues>,
pub text: Vec<std::sync::Arc<GlyphStore>>,
pub font_metrics: FontMetrics,
pub font_key: FontInstanceKey,
pub text_decoration_line: TextDecorationLine,
pub bidi_level: Level,
}
impl TextRunLineItem {
fn trim_whitespace_at_end(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}
let index_of_last_non_whitespace = self
.text
.iter()
.rev()
.position(|glyph| !glyph.is_whitespace())
.map(|offset_from_end| self.text.len() - offset_from_end);
let first_whitespace_index = index_of_last_non_whitespace.unwrap_or(0);
*whitespace_trimmed += self
.text
.drain(first_whitespace_index..)
.map(|glyph| glyph.total_advance())
.sum();
index_of_last_non_whitespace.is_none()
}
fn trim_whitespace_at_start(&mut self, whitespace_trimmed: &mut Au) -> bool {
if matches!(
self.parent_style.get_inherited_text().white_space_collapse,
WhiteSpaceCollapse::Preserve | WhiteSpaceCollapse::BreakSpaces
) {
return false;
}
let index_of_first_non_whitespace = self
.text
.iter()
.position(|glyph| !glyph.is_whitespace())
.unwrap_or(self.text.len());
*whitespace_trimmed += self
.text
.drain(0..index_of_first_non_whitespace)
.map(|glyph| glyph.total_advance())
.sum();
self.text.is_empty()
}
pub(crate) fn can_merge(&self, font_key: FontInstanceKey, bidi_level: Level) -> bool {
self.font_key == font_key && self.bidi_level == bidi_level
}
}
pub(super) struct AtomicLineItem {
pub fragment: BoxFragment,
pub size: LogicalVec2<Au>,
pub positioning_context: Option<PositioningContext>,
pub baseline_offset_in_parent: Au,
pub baseline_offset_in_item: Au,
pub bidi_level: Level,
}
impl AtomicLineItem {
fn calculate_block_start(&self, line_metrics: &LineMetrics) -> Au {
match self.fragment.style.clone_vertical_align() {
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Top) => Au::zero(),
GenericVerticalAlign::Keyword(VerticalAlignKeyword::Bottom) => {
line_metrics.block_size - self.size.block
},
_ => {
let baseline = line_metrics.baseline_block_offset + self.baseline_offset_in_parent;
baseline - self.baseline_offset_in_item
},
}
}
}
pub(super) struct AbsolutelyPositionedLineItem {
pub absolutely_positioned_box: ArcRefCell<AbsolutelyPositionedBox>,
}
pub(super) struct FloatLineItem {
pub fragment: BoxFragment,
pub needs_placement: bool,
}
fn line_height(parent_style: &ComputedValues, font_metrics: &FontMetrics) -> Au {
let font = parent_style.get_font();
let font_size = font.font_size.computed_size();
match font.line_height {
LineHeight::Normal => font_metrics.line_gap,
LineHeight::Number(number) => (font_size * number.0).into(),
LineHeight::Length(length) => length.0.into(),
}
}
fn sort_by_indices_in_place<T>(data: &mut [T], mut indices: Vec<usize>) {
for idx in 0..data.len() {
if indices[idx] == idx {
continue;
}
let mut current_idx = idx;
loop {
let target_idx = indices[current_idx];
indices[current_idx] = current_idx;
if indices[target_idx] == target_idx {
break;
}
data.swap(current_idx, target_idx);
current_idx = target_idx;
}
}
}