diff options
-rw-r--r-- | components/layout_2020/dom_traversal.rs | 344 | ||||
-rw-r--r-- | components/layout_2020/element_data.rs | 19 | ||||
-rw-r--r-- | components/layout_2020/flow/construct.rs | 625 | ||||
-rw-r--r-- | components/layout_2020/flow/float.rs | 18 | ||||
-rw-r--r-- | components/layout_2020/flow/inline.rs | 345 | ||||
-rw-r--r-- | components/layout_2020/flow/mod.rs | 439 | ||||
-rw-r--r-- | components/layout_2020/flow/root.rs | 122 | ||||
-rw-r--r-- | components/layout_2020/fragments.rs | 116 | ||||
-rw-r--r-- | components/layout_2020/geom.rs | 330 | ||||
-rw-r--r-- | components/layout_2020/positioned.rs | 328 | ||||
-rw-r--r-- | components/layout_2020/primitives.rs | 28 | ||||
-rw-r--r-- | components/layout_2020/replaced.rs | 14 |
12 files changed, 2728 insertions, 0 deletions
diff --git a/components/layout_2020/dom_traversal.rs b/components/layout_2020/dom_traversal.rs new file mode 100644 index 00000000000..848e89ba564 --- /dev/null +++ b/components/layout_2020/dom_traversal.rs @@ -0,0 +1,344 @@ +use super::*; +use crate::dom::{Document, NodeData, NodeId}; +use crate::style::StyleSet; +use atomic_refcell::AtomicRefMut; + +pub(super) struct Context<'a> { + pub document: &'a Document, + pub author_styles: &'a StyleSet, +} + +#[derive(Copy, Clone)] +pub(super) enum WhichPseudoElement { + Before, + After, +} + +pub(super) enum Contents { + /// Refers to a DOM subtree, plus `::before` and `::after` pseudo-elements. + OfElement(NodeId), + + /// Example: an `<img src=…>` element. + /// <https://drafts.csswg.org/css2/conform.html#replaced-element> + Replaced(ReplacedContent), + + /// Content of a `::before` or `::after` pseudo-element this is being generated. + /// <https://drafts.csswg.org/css2/generate.html#content> + OfPseudoElement(Vec<PseudoElementContentItem>), +} + +pub(super) enum NonReplacedContents { + OfElement(NodeId), + OfPseudoElement(Vec<PseudoElementContentItem>), +} + +pub(super) enum PseudoElementContentItem { + Text(String), + Replaced(ReplacedContent), +} + +pub(super) trait TraversalHandler<'dom> { + fn handle_text(&mut self, text: &str, parent_style: &Arc<ComputedValues>); + + /// Or pseudo-element + fn handle_element( + &mut self, + style: &Arc<ComputedValues>, + display: DisplayGeneratingBox, + contents: Contents, + box_slot: BoxSlot<'dom>, + ); +} + +fn traverse_children_of<'dom>( + parent_element: NodeId, + parent_element_style: &Arc<ComputedValues>, + context: &'dom Context, + handler: &mut impl TraversalHandler<'dom>, +) { + traverse_pseudo_element( + WhichPseudoElement::Before, + parent_element, + parent_element_style, + context, + handler, + ); + + let mut next = context.document[parent_element].first_child; + while let Some(child) = next { + match &context.document[child].data { + NodeData::Document + | NodeData::Doctype { .. } + | NodeData::Comment { .. } + | NodeData::ProcessingInstruction { .. } => {} + NodeData::Text { contents } => { + handler.handle_text(contents, parent_element_style); + } + NodeData::Element(_) => traverse_element(child, parent_element_style, context, handler), + } + next = context.document[child].next_sibling + } + + traverse_pseudo_element( + WhichPseudoElement::After, + parent_element, + &parent_element_style, + context, + handler, + ); +} + +fn traverse_element<'dom>( + element_id: NodeId, + parent_element_style: &ComputedValues, + context: &'dom Context, + handler: &mut impl TraversalHandler<'dom>, +) { + let style = style_for_element( + context.author_styles, + context.document, + element_id, + Some(parent_element_style), + ); + match style.box_.display { + Display::None => context.unset_boxes_in_subtree(element_id), + Display::Contents => { + if ReplacedContent::for_element(element_id, context).is_some() { + // `display: content` on a replaced element computes to `display: none` + // <https://drafts.csswg.org/css-display-3/#valdef-display-contents> + context.unset_boxes_in_subtree(element_id) + } else { + context.layout_data_mut(element_id).self_box = Some(LayoutBox::DisplayContents); + traverse_children_of(element_id, &style, context, handler) + } + } + Display::GeneratingBox(display) => handler.handle_element( + &style, + display, + match ReplacedContent::for_element(element_id, context) { + Some(replaced) => Contents::Replaced(replaced), + None => Contents::OfElement(element_id), + }, + context.element_box_slot(element_id), + ), + } +} + +fn traverse_pseudo_element<'dom>( + which: WhichPseudoElement, + element: NodeId, + element_style: &ComputedValues, + context: &'dom Context, + handler: &mut impl TraversalHandler<'dom>, +) { + if let Some(style) = pseudo_element_style(which, element, element_style, context) { + match style.box_.display { + Display::None => context.unset_pseudo_element_box(element, which), + Display::Contents => { + context.unset_pseudo_element_box(element, which); + let items = generate_pseudo_element_content(&style, element, context); + traverse_pseudo_element_contents(&style, items, handler); + } + Display::GeneratingBox(display) => { + let items = generate_pseudo_element_content(&style, element, context); + let contents = Contents::OfPseudoElement(items); + let box_slot = context.pseudo_element_box_slot(element, which); + handler.handle_element(&style, display, contents, box_slot); + } + } + } +} + +fn traverse_pseudo_element_contents<'dom>( + pseudo_element_style: &Arc<ComputedValues>, + items: Vec<PseudoElementContentItem>, + handler: &mut impl TraversalHandler<'dom>, +) { + let mut anonymous_style = None; + for item in items { + match item { + PseudoElementContentItem::Text(text) => { + handler.handle_text(&text, pseudo_element_style) + } + PseudoElementContentItem::Replaced(contents) => { + let item_style = anonymous_style.get_or_insert_with(|| { + ComputedValues::anonymous_inheriting_from(Some(pseudo_element_style)) + }); + let display_inline = DisplayGeneratingBox::OutsideInside { + outside: DisplayOutside::Inline, + inside: DisplayInside::Flow, + }; + // `display` is not inherited, so we get the initial value + debug_assert!(item_style.box_.display == Display::GeneratingBox(display_inline)); + handler.handle_element( + item_style, + display_inline, + Contents::Replaced(contents), + // We don’t keep pointers to boxes generated by contents of pseudo-elements + BoxSlot::dummy(), + ) + } + } + } +} + +impl std::convert::TryFrom<Contents> for NonReplacedContents { + type Error = ReplacedContent; + + fn try_from(contents: Contents) -> Result<Self, Self::Error> { + match contents { + Contents::OfElement(id) => Ok(NonReplacedContents::OfElement(id)), + Contents::OfPseudoElement(items) => Ok(NonReplacedContents::OfPseudoElement(items)), + Contents::Replaced(replaced) => Err(replaced), + } + } +} + +impl std::convert::From<NonReplacedContents> for Contents { + fn from(contents: NonReplacedContents) -> Self { + match contents { + NonReplacedContents::OfElement(id) => Contents::OfElement(id), + NonReplacedContents::OfPseudoElement(items) => Contents::OfPseudoElement(items), + } + } +} + +impl NonReplacedContents { + pub fn traverse<'dom>( + self, + inherited_style: &Arc<ComputedValues>, + context: &'dom Context, + handler: &mut impl TraversalHandler<'dom>, + ) { + match self { + NonReplacedContents::OfElement(id) => { + traverse_children_of(id, inherited_style, context, handler) + } + NonReplacedContents::OfPseudoElement(items) => { + traverse_pseudo_element_contents(inherited_style, items, handler) + } + } + } +} + +fn pseudo_element_style( + _which: WhichPseudoElement, + _element: NodeId, + _element_style: &ComputedValues, + _context: &Context, +) -> Option<Arc<ComputedValues>> { + // FIXME: run the cascade, then return None for `content: normal` or `content: none` + // https://drafts.csswg.org/css2/generate.html#content + None +} + +fn generate_pseudo_element_content( + _pseudo_element_style: &ComputedValues, + _element: NodeId, + _context: &Context, +) -> Vec<PseudoElementContentItem> { + let _ = PseudoElementContentItem::Text; + let _ = PseudoElementContentItem::Replaced; + unimplemented!() +} + +pub(super) struct BoxSlot<'dom> { + slot: Option<AtomicRefMut<'dom, Option<LayoutBox>>>, +} + +impl<'dom> BoxSlot<'dom> { + pub fn new(mut slot: AtomicRefMut<'dom, Option<LayoutBox>>) -> Self { + *slot = None; + Self { slot: Some(slot) } + } + + pub fn dummy() -> Self { + Self { slot: None } + } + + pub fn set(mut self, box_: LayoutBox) { + if let Some(slot) = &mut self.slot { + **slot = Some(box_) + } + } +} + +impl Drop for BoxSlot<'_> { + fn drop(&mut self) { + if let Some(slot) = &mut self.slot { + assert!(slot.is_some(), "failed to set a layout box") + } + } +} + +impl Context<'_> { + fn layout_data_mut(&self, element_id: NodeId) -> AtomicRefMut<LayoutDataForElement> { + self.document[element_id] + .as_element() + .unwrap() + .layout_data + .borrow_mut() + } + + fn element_box_slot(&self, element_id: NodeId) -> BoxSlot { + BoxSlot::new(AtomicRefMut::map( + self.layout_data_mut(element_id), + |data| &mut data.self_box, + )) + } + + fn pseudo_element_box_slot(&self, element_id: NodeId, which: WhichPseudoElement) -> BoxSlot { + BoxSlot::new(AtomicRefMut::map( + self.layout_data_mut(element_id), + |data| { + let pseudos = data.pseudo_elements.get_or_insert_with(Default::default); + match which { + WhichPseudoElement::Before => &mut pseudos.before, + WhichPseudoElement::After => &mut pseudos.after, + } + }, + )) + } + + fn unset_pseudo_element_box(&self, element_id: NodeId, which: WhichPseudoElement) { + if let Some(pseudos) = &mut self.layout_data_mut(element_id).pseudo_elements { + match which { + WhichPseudoElement::Before => pseudos.before = None, + WhichPseudoElement::After => pseudos.after = None, + } + } + } + + fn unset_boxes_in_subtree(&self, base_element: NodeId) { + let mut node_id = base_element; + loop { + let node = &self.document[node_id]; + if let Some(element_data) = node.as_element() { + let mut layout_data = element_data.layout_data.borrow_mut(); + layout_data.pseudo_elements = None; + if layout_data.self_box.take().is_some() { + // Only descend into children if we removed a box. + // If there wasn’t one, then descendants don’t have boxes either. + if let Some(child) = node.first_child { + node_id = child; + continue; + } + } + } + let mut next_is_a_sibling_of = node_id; + node_id = loop { + if let Some(sibling) = self.document[next_is_a_sibling_of].next_sibling { + break sibling; + } else { + next_is_a_sibling_of = node + .parent + .expect("reached the root while traversing only a subtree"); + } + }; + if next_is_a_sibling_of == base_element { + // Don’t go outside the subtree + return; + } + } + } +} diff --git a/components/layout_2020/element_data.rs b/components/layout_2020/element_data.rs new file mode 100644 index 00000000000..bda0d3cbe9e --- /dev/null +++ b/components/layout_2020/element_data.rs @@ -0,0 +1,19 @@ +use super::*; + +#[derive(Default)] +pub(crate) struct LayoutDataForElement { + pub(super) self_box: Option<LayoutBox>, + pub(super) pseudo_elements: Option<Box<PseudoElementBoxes>>, +} + +#[derive(Default)] +pub(super) struct PseudoElementBoxes { + pub before: Option<LayoutBox>, + pub after: Option<LayoutBox>, +} + +pub(super) enum LayoutBox { + DisplayContents, + BlockLevel(Arc<BlockLevelBox>), + InlineLevel(Arc<InlineLevelBox>), +} diff --git a/components/layout_2020/flow/construct.rs b/components/layout_2020/flow/construct.rs new file mode 100644 index 00000000000..4acfc93ce92 --- /dev/null +++ b/components/layout_2020/flow/construct.rs @@ -0,0 +1,625 @@ +use super::*; + +impl BlockFormattingContext { + pub fn construct<'a>( + context: &'a Context<'a>, + style: &'a Arc<ComputedValues>, + contents: NonReplacedContents, + ) -> Self { + let (contents, contains_floats) = BlockContainer::construct(context, style, contents); + Self { + contents, + contains_floats: contains_floats == ContainsFloats::Yes, + } + } +} + +enum IntermediateBlockLevelBox { + SameFormattingContextBlock { + style: Arc<ComputedValues>, + contents: IntermediateBlockContainer, + }, + Independent { + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + }, + OutOfFlowAbsolutelyPositionedBox { + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + }, + OutOfFlowFloatBox { + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + }, +} + +/// A block container that may still have to be constructed. +/// +/// Represents either the inline formatting context of an anonymous block +/// box or the yet-to-be-computed block container generated from the children +/// of a given element. +/// +/// Deferring allows using rayon’s `into_par_iter`. +enum IntermediateBlockContainer { + InlineFormattingContext(InlineFormattingContext), + Deferred { contents: NonReplacedContents }, +} + +/// A builder for a block container. +/// +/// This builder starts from the first child of a given DOM node +/// and does a preorder traversal of all of its inclusive siblings. +struct BlockContainerBuilder<'a> { + context: &'a Context<'a>, + block_container_style: &'a Arc<ComputedValues>, + + /// The list of block-level boxes of the final block container. + /// + /// Contains all the complete block level boxes we found traversing the tree + /// so far, if this is empty at the end of the traversal and the ongoing + /// inline formatting context is not empty, the block container establishes + /// an inline formatting context (see end of `build`). + /// + /// DOM nodes which represent block-level boxes are immediately pushed + /// to this list with their style without ever being traversed at this + /// point, instead we just move to their next sibling. If the DOM node + /// doesn't have a next sibling, we either reached the end of the container + /// root or there are ongoing inline-level boxes + /// (see `handle_block_level_element`). + block_level_boxes: Vec<(IntermediateBlockLevelBox, BoxSlot<'a>)>, + + /// The ongoing inline formatting context of the builder. + /// + /// Contains all the complete inline level boxes we found traversing the + /// tree so far. If a block-level box is found during traversal, + /// this inline formatting context is pushed as a block level box to + /// the list of block-level boxes of the builder + /// (see `end_ongoing_inline_formatting_context`). + ongoing_inline_formatting_context: InlineFormattingContext, + + /// The ongoing stack of inline boxes stack of the builder. + /// + /// Contains all the currently ongoing inline boxes we entered so far. + /// The traversal is at all times as deep in the tree as this stack is, + /// which is why the code doesn't need to keep track of the actual + /// container root (see `handle_inline_level_element`). + /// + /// Whenever the end of a DOM element that represents an inline box is + /// reached, the inline box at the top of this stack is complete and ready + /// to be pushed to the children of the next last ongoing inline box + /// the ongoing inline formatting context if the stack is now empty, + /// which means we reached the end of a child of the actual + /// container root (see `move_to_next_sibling`). + ongoing_inline_boxes_stack: Vec<InlineBox>, + + /// The style of the anonymous block boxes pushed to the list of block-level + /// boxes, if any (see `end_ongoing_inline_formatting_context`). + anonymous_style: Option<Arc<ComputedValues>>, + + /// Whether the resulting block container contains any float box. + contains_floats: ContainsFloats, +} + +impl BlockContainer { + pub fn construct<'a>( + context: &'a Context<'a>, + block_container_style: &'a Arc<ComputedValues>, + contents: NonReplacedContents, + ) -> (BlockContainer, ContainsFloats) { + let mut builder = BlockContainerBuilder { + context, + block_container_style, + block_level_boxes: Default::default(), + ongoing_inline_formatting_context: Default::default(), + ongoing_inline_boxes_stack: Default::default(), + anonymous_style: Default::default(), + contains_floats: Default::default(), + }; + + contents.traverse(block_container_style, context, &mut builder); + + debug_assert!(builder.ongoing_inline_boxes_stack.is_empty()); + + if !builder + .ongoing_inline_formatting_context + .inline_level_boxes + .is_empty() + { + if builder.block_level_boxes.is_empty() { + let container = BlockContainer::InlineFormattingContext( + builder.ongoing_inline_formatting_context, + ); + return (container, builder.contains_floats); + } + builder.end_ongoing_inline_formatting_context(); + } + + let mut contains_floats = builder.contains_floats; + let container = BlockContainer::BlockLevelBoxes( + builder + .block_level_boxes + .into_par_iter() + .mapfold_reduce_into( + &mut contains_floats, + |contains_floats, (intermediate, box_slot)| { + let (block_level_box, box_contains_floats) = intermediate.finish(context); + *contains_floats |= box_contains_floats; + box_slot.set(LayoutBox::BlockLevel(block_level_box.clone())); + block_level_box + }, + |left, right| *left |= right, + ) + .collect(), + ); + (container, contains_floats) + } +} + +impl<'a> TraversalHandler<'a> for BlockContainerBuilder<'a> { + fn handle_element( + &mut self, + style: &Arc<ComputedValues>, + display: DisplayGeneratingBox, + contents: Contents, + box_slot: BoxSlot<'a>, + ) { + match display { + DisplayGeneratingBox::OutsideInside { outside, inside } => match outside { + DisplayOutside::Inline => box_slot.set(LayoutBox::InlineLevel( + self.handle_inline_level_element(style, inside, contents), + )), + DisplayOutside::Block => { + // Floats and abspos cause blockification, so they only happen in this case. + // https://drafts.csswg.org/css2/visuren.html#dis-pos-flo + if style.box_.position.is_absolutely_positioned() { + self.handle_absolutely_positioned_element( + style.clone(), + inside, + contents, + box_slot, + ) + } else if style.box_.float.is_floating() { + self.handle_float_element(style.clone(), inside, contents, box_slot) + } else { + self.handle_block_level_element(style.clone(), inside, contents, box_slot) + } + } + }, + } + } + + fn handle_text(&mut self, input: &str, parent_style: &Arc<ComputedValues>) { + let (leading_whitespace, mut input) = self.handle_leading_whitespace(input); + if leading_whitespace || !input.is_empty() { + // This text node should be pushed either to the next ongoing + // inline level box with the parent style of that inline level box + // that will be ended, or directly to the ongoing inline formatting + // context with the parent style of that builder. + let inlines = self.current_inline_level_boxes(); + + fn last_text(inlines: &mut [Arc<InlineLevelBox>]) -> Option<&mut String> { + let last = inlines.last_mut()?; + if let InlineLevelBox::TextRun(_) = &**last { + // We never clone text run boxes, so the refcount is 1 and unwrap succeeds: + let last = Arc::get_mut(last).unwrap(); + if let InlineLevelBox::TextRun(TextRun { text, .. }) = last { + Some(text) + } else { + unreachable!() + } + } else { + None + } + } + + let mut new_text_run_contents; + let output; + if let Some(text) = last_text(inlines) { + // Append to the existing text run + new_text_run_contents = None; + output = text; + } else { + new_text_run_contents = Some(String::new()); + output = new_text_run_contents.as_mut().unwrap(); + } + + if leading_whitespace { + output.push(' ') + } + loop { + if let Some(i) = input.bytes().position(|b| b.is_ascii_whitespace()) { + let (non_whitespace, rest) = input.split_at(i); + output.push_str(non_whitespace); + output.push(' '); + if let Some(i) = rest.bytes().position(|b| !b.is_ascii_whitespace()) { + input = &rest[i..]; + } else { + break; + } + } else { + output.push_str(input); + break; + } + } + + if let Some(text) = new_text_run_contents { + let parent_style = parent_style.clone(); + inlines.push(Arc::new(InlineLevelBox::TextRun(TextRun { + parent_style, + text, + }))) + } + } + } +} + +impl<'a> BlockContainerBuilder<'a> { + /// Returns: + /// + /// * Whether this text run has preserved (non-collapsible) leading whitespace + /// * The contents starting at the first non-whitespace character (or the empty string) + fn handle_leading_whitespace<'text>(&mut self, text: &'text str) -> (bool, &'text str) { + // FIXME: this is only an approximation of + // https://drafts.csswg.org/css2/text.html#white-space-model + if !text.starts_with(|c: char| c.is_ascii_whitespace()) { + return (false, text); + } + let mut inline_level_boxes = self.current_inline_level_boxes().iter().rev(); + let mut stack = Vec::new(); + let preserved = loop { + match inline_level_boxes.next().map(|b| &**b) { + Some(InlineLevelBox::TextRun(r)) => break !r.text.ends_with(' '), + Some(InlineLevelBox::Atomic { .. }) => break false, + Some(InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(_)) + | Some(InlineLevelBox::OutOfFlowFloatBox(_)) => {} + Some(InlineLevelBox::InlineBox(b)) => { + stack.push(inline_level_boxes); + inline_level_boxes = b.children.iter().rev() + } + None => { + if let Some(iter) = stack.pop() { + inline_level_boxes = iter + } else { + break false; // Paragraph start + } + } + } + }; + let text = text.trim_start_matches(|c: char| c.is_ascii_whitespace()); + (preserved, text) + } + + fn handle_inline_level_element( + &mut self, + style: &Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + ) -> Arc<InlineLevelBox> { + let box_ = match contents.try_into() { + Err(replaced) => Arc::new(InlineLevelBox::Atomic { + style: style.clone(), + contents: replaced, + }), + Ok(non_replaced) => match display_inside { + DisplayInside::Flow => { + // Whatever happened before, we just found an inline level element, so + // all we need to do is to remember this ongoing inline level box. + self.ongoing_inline_boxes_stack.push(InlineBox { + style: style.clone(), + first_fragment: true, + last_fragment: false, + children: vec![], + }); + + NonReplacedContents::traverse(non_replaced, &style, self.context, self); + + let mut inline_box = self + .ongoing_inline_boxes_stack + .pop() + .expect("no ongoing inline level box found"); + inline_box.last_fragment = true; + Arc::new(InlineLevelBox::InlineBox(inline_box)) + } + DisplayInside::FlowRoot => { + // a.k.a. `inline-block` + unimplemented!() + } + }, + }; + self.current_inline_level_boxes().push(box_.clone()); + box_ + } + + fn handle_block_level_element( + &mut self, + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'a>, + ) { + // We just found a block level element, all ongoing inline level boxes + // need to be split around it. We iterate on the fragmented inline + // level box stack to take their contents and set their first_fragment + // field to false, for the fragmented inline level boxes that will + // come after the block level element. + let mut fragmented_inline_boxes = + self.ongoing_inline_boxes_stack + .iter_mut() + .rev() + .map(|ongoing| { + let fragmented = InlineBox { + style: ongoing.style.clone(), + first_fragment: ongoing.first_fragment, + // The fragmented boxes before the block level element + // are obviously not the last fragment. + last_fragment: false, + children: take(&mut ongoing.children), + }; + ongoing.first_fragment = false; + fragmented + }); + + if let Some(last) = fragmented_inline_boxes.next() { + // There were indeed some ongoing inline level boxes before + // the block, we accumulate them as a single inline level box + // to be pushed to the ongoing inline formatting context. + let mut fragmented_inline = InlineLevelBox::InlineBox(last); + for mut fragmented_parent_inline_box in fragmented_inline_boxes { + fragmented_parent_inline_box + .children + .push(Arc::new(fragmented_inline)); + fragmented_inline = InlineLevelBox::InlineBox(fragmented_parent_inline_box); + } + + self.ongoing_inline_formatting_context + .inline_level_boxes + .push(Arc::new(fragmented_inline)); + } + + // We found a block level element, so the ongoing inline formatting + // context needs to be ended. + self.end_ongoing_inline_formatting_context(); + + let intermediate_box = match contents.try_into() { + Ok(contents) => match display_inside { + DisplayInside::Flow => IntermediateBlockLevelBox::SameFormattingContextBlock { + style, + contents: IntermediateBlockContainer::Deferred { contents }, + }, + _ => IntermediateBlockLevelBox::Independent { + style, + display_inside, + contents: contents.into(), + }, + }, + Err(contents) => { + let contents = Contents::Replaced(contents); + IntermediateBlockLevelBox::Independent { + style, + display_inside, + contents, + } + } + }; + self.block_level_boxes.push((intermediate_box, box_slot)) + } + + fn handle_absolutely_positioned_element( + &mut self, + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'a>, + ) { + if !self.has_ongoing_inline_formatting_context() { + let box_ = IntermediateBlockLevelBox::OutOfFlowAbsolutelyPositionedBox { + style, + contents, + display_inside, + }; + self.block_level_boxes.push((box_, box_slot)) + } else { + let box_ = Arc::new(InlineLevelBox::OutOfFlowAbsolutelyPositionedBox( + AbsolutelyPositionedBox { + contents: IndependentFormattingContext::construct( + self.context, + &style, + display_inside, + contents, + ), + style, + }, + )); + self.current_inline_level_boxes().push(box_.clone()); + box_slot.set(LayoutBox::InlineLevel(box_)) + } + } + + fn handle_float_element( + &mut self, + style: Arc<ComputedValues>, + display_inside: DisplayInside, + contents: Contents, + box_slot: BoxSlot<'a>, + ) { + self.contains_floats = ContainsFloats::Yes; + + if !self.has_ongoing_inline_formatting_context() { + let box_ = IntermediateBlockLevelBox::OutOfFlowFloatBox { + style, + contents, + display_inside, + }; + self.block_level_boxes.push((box_, box_slot)); + } else { + let box_ = Arc::new(InlineLevelBox::OutOfFlowFloatBox(FloatBox { + contents: IndependentFormattingContext::construct( + self.context, + &style, + display_inside, + contents, + ), + style, + })); + self.current_inline_level_boxes().push(box_.clone()); + box_slot.set(LayoutBox::InlineLevel(box_)) + } + } + + fn end_ongoing_inline_formatting_context(&mut self) { + assert!( + self.ongoing_inline_boxes_stack.is_empty(), + "there should be no ongoing inline level boxes", + ); + + if self + .ongoing_inline_formatting_context + .inline_level_boxes + .is_empty() + { + // There should never be an empty inline formatting context. + return; + } + + let block_container_style = self.block_container_style; + let anonymous_style = self.anonymous_style.get_or_insert_with(|| { + // If parent_style is None, the parent is the document node, + // in which case anonymous inline boxes should inherit their + // styles from initial values. + ComputedValues::anonymous_inheriting_from(Some(block_container_style)) + }); + + let box_ = IntermediateBlockLevelBox::SameFormattingContextBlock { + style: anonymous_style.clone(), + contents: IntermediateBlockContainer::InlineFormattingContext(take( + &mut self.ongoing_inline_formatting_context, + )), + }; + self.block_level_boxes.push((box_, BoxSlot::dummy())) + } + + fn current_inline_level_boxes(&mut self) -> &mut Vec<Arc<InlineLevelBox>> { + match self.ongoing_inline_boxes_stack.last_mut() { + Some(last) => &mut last.children, + None => &mut self.ongoing_inline_formatting_context.inline_level_boxes, + } + } + + fn has_ongoing_inline_formatting_context(&self) -> bool { + !self + .ongoing_inline_formatting_context + .inline_level_boxes + .is_empty() + || !self.ongoing_inline_boxes_stack.is_empty() + } +} + +impl IntermediateBlockLevelBox { + fn finish(self, context: &Context) -> (Arc<BlockLevelBox>, ContainsFloats) { + match self { + IntermediateBlockLevelBox::SameFormattingContextBlock { style, contents } => { + let (contents, contains_floats) = contents.finish(context, &style); + let block_level_box = + Arc::new(BlockLevelBox::SameFormattingContextBlock { contents, style }); + (block_level_box, contains_floats) + } + IntermediateBlockLevelBox::Independent { + style, + display_inside, + contents, + } => { + let contents = IndependentFormattingContext::construct( + context, + &style, + display_inside, + contents, + ); + ( + Arc::new(BlockLevelBox::Independent { style, contents }), + ContainsFloats::No, + ) + } + IntermediateBlockLevelBox::OutOfFlowAbsolutelyPositionedBox { + style, + display_inside, + contents, + } => { + let block_level_box = Arc::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( + AbsolutelyPositionedBox { + contents: IndependentFormattingContext::construct( + context, + &style, + display_inside, + contents, + ), + style: style, + }, + )); + (block_level_box, ContainsFloats::No) + } + IntermediateBlockLevelBox::OutOfFlowFloatBox { + style, + display_inside, + contents, + } => { + let contents = IndependentFormattingContext::construct( + context, + &style, + display_inside, + contents, + ); + let block_level_box = Arc::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox { + contents, + style, + })); + (block_level_box, ContainsFloats::Yes) + } + } + } +} + +impl IntermediateBlockContainer { + fn finish( + self, + context: &Context, + style: &Arc<ComputedValues>, + ) -> (BlockContainer, ContainsFloats) { + match self { + IntermediateBlockContainer::Deferred { contents } => { + BlockContainer::construct(context, style, contents) + } + IntermediateBlockContainer::InlineFormattingContext(ifc) => { + // If that inline formatting context contained any float, those + // were already taken into account during the first phase of + // box construction. + ( + BlockContainer::InlineFormattingContext(ifc), + ContainsFloats::No, + ) + } + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(in crate::layout) enum ContainsFloats { + No, + Yes, +} + +impl std::ops::BitOrAssign for ContainsFloats { + fn bitor_assign(&mut self, other: Self) { + if other == ContainsFloats::Yes { + *self = ContainsFloats::Yes; + } + } +} + +impl Default for ContainsFloats { + fn default() -> Self { + ContainsFloats::No + } +} diff --git a/components/layout_2020/flow/float.rs b/components/layout_2020/flow/float.rs new file mode 100644 index 00000000000..1c06b5cf2da --- /dev/null +++ b/components/layout_2020/flow/float.rs @@ -0,0 +1,18 @@ +use super::*; + +#[derive(Debug)] +pub(in crate::layout) struct FloatBox { + pub style: Arc<ComputedValues>, + pub contents: IndependentFormattingContext, +} + +/// Data kept during layout about the floats in a given block formatting context. +pub(in crate::layout) struct FloatContext { + // TODO +} + +impl FloatContext { + pub fn new() -> Self { + FloatContext {} + } +} diff --git a/components/layout_2020/flow/inline.rs b/components/layout_2020/flow/inline.rs new file mode 100644 index 00000000000..5c39bb9385b --- /dev/null +++ b/components/layout_2020/flow/inline.rs @@ -0,0 +1,345 @@ +use super::*; +use crate::fonts::BITSTREAM_VERA_SANS; +use crate::text::ShapedSegment; + +#[derive(Debug, Default)] +pub(in crate::layout) struct InlineFormattingContext { + pub(super) inline_level_boxes: Vec<Arc<InlineLevelBox>>, +} + +#[derive(Debug)] +pub(in crate::layout) enum InlineLevelBox { + InlineBox(InlineBox), + TextRun(TextRun), + OutOfFlowAbsolutelyPositionedBox(AbsolutelyPositionedBox), + OutOfFlowFloatBox(FloatBox), + Atomic { + style: Arc<ComputedValues>, + // FIXME: this should be IndependentFormattingContext: + contents: ReplacedContent, + }, +} + +#[derive(Debug)] +pub(in crate::layout) struct InlineBox { + pub style: Arc<ComputedValues>, + pub first_fragment: bool, + pub last_fragment: bool, + pub children: Vec<Arc<InlineLevelBox>>, +} + +/// https://www.w3.org/TR/css-display-3/#css-text-run +#[derive(Debug)] +pub(in crate::layout) struct TextRun { + pub parent_style: Arc<ComputedValues>, + pub text: String, +} + +struct InlineNestingLevelState<'box_tree> { + remaining_boxes: std::slice::Iter<'box_tree, Arc<InlineLevelBox>>, + fragments_so_far: Vec<Fragment>, + inline_start: Length, + max_block_size_of_fragments_so_far: Length, +} + +struct PartialInlineBoxFragment<'box_tree> { + style: Arc<ComputedValues>, + start_corner: Vec2<Length>, + padding: Sides<Length>, + border: Sides<Length>, + margin: Sides<Length>, + last_box_tree_fragment: bool, + parent_nesting_level: InlineNestingLevelState<'box_tree>, +} + +struct InlineFormattingContextState<'box_tree, 'cb> { + containing_block: &'cb ContainingBlock, + line_boxes: LinesBoxes, + inline_position: Length, + partial_inline_boxes_stack: Vec<PartialInlineBoxFragment<'box_tree>>, + current_nesting_level: InlineNestingLevelState<'box_tree>, +} + +struct LinesBoxes { + boxes: Vec<Fragment>, + next_line_block_position: Length, +} + +impl InlineFormattingContext { + pub(super) fn layout<'a>( + &'a self, + containing_block: &ContainingBlock, + tree_rank: usize, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + ) -> FlowChildren { + let mut ifc = InlineFormattingContextState { + containing_block, + partial_inline_boxes_stack: Vec::new(), + line_boxes: LinesBoxes { + boxes: Vec::new(), + next_line_block_position: Length::zero(), + }, + inline_position: Length::zero(), + current_nesting_level: InlineNestingLevelState { + remaining_boxes: self.inline_level_boxes.iter(), + fragments_so_far: Vec::with_capacity(self.inline_level_boxes.len()), + inline_start: Length::zero(), + max_block_size_of_fragments_so_far: Length::zero(), + }, + }; + loop { + if let Some(child) = ifc.current_nesting_level.remaining_boxes.next() { + match &**child { + InlineLevelBox::InlineBox(inline) => { + let partial = inline.start_layout(&mut ifc); + ifc.partial_inline_boxes_stack.push(partial) + } + InlineLevelBox::TextRun(run) => run.layout(&mut ifc), + InlineLevelBox::Atomic { style: _, contents } => { + // FIXME + match *contents {} + } + InlineLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => { + let initial_start_corner = match box_.style.specified_display { + Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { + outside, + inside: _, + }) => Vec2 { + inline: match outside { + DisplayOutside::Inline => ifc.inline_position, + DisplayOutside::Block => Length::zero(), + }, + block: ifc.line_boxes.next_line_block_position, + }, + Display::Contents => { + panic!("display:contents does not generate an abspos box") + } + Display::None => panic!("display:none does not generate an abspos box"), + }; + absolutely_positioned_fragments + .push(box_.layout(initial_start_corner, tree_rank)); + } + InlineLevelBox::OutOfFlowFloatBox(_box_) => { + // TODO + continue; + } + } + } else + // Reached the end of ifc.remaining_boxes + if let Some(mut partial) = ifc.partial_inline_boxes_stack.pop() { + partial.finish_layout( + &mut ifc.current_nesting_level, + &mut ifc.inline_position, + false, + ); + ifc.current_nesting_level = partial.parent_nesting_level + } else { + ifc.line_boxes + .finish_line(&mut ifc.current_nesting_level, containing_block); + return FlowChildren { + fragments: ifc.line_boxes.boxes, + block_size: ifc.line_boxes.next_line_block_position, + collapsible_margins_in_children: CollapsedBlockMargins::zero(), + }; + } + } + } +} + +impl LinesBoxes { + fn finish_line( + &mut self, + top_nesting_level: &mut InlineNestingLevelState, + containing_block: &ContainingBlock, + ) { + let start_corner = Vec2 { + inline: Length::zero(), + block: self.next_line_block_position, + }; + let size = Vec2 { + inline: containing_block.inline_size, + block: std::mem::replace( + &mut top_nesting_level.max_block_size_of_fragments_so_far, + Length::zero(), + ), + }; + self.next_line_block_position += size.block; + self.boxes.push(Fragment::Anonymous(AnonymousFragment { + children: take(&mut top_nesting_level.fragments_so_far), + rect: Rect { start_corner, size }, + mode: containing_block.mode, + })) + } +} + +impl InlineBox { + fn start_layout<'box_tree>( + &'box_tree self, + ifc: &mut InlineFormattingContextState<'box_tree, '_>, + ) -> PartialInlineBoxFragment<'box_tree> { + let style = self.style.clone(); + let cbis = ifc.containing_block.inline_size; + let mut padding = style.padding().percentages_relative_to(cbis); + let mut border = style.border_width().percentages_relative_to(cbis); + let mut margin = style + .margin() + .percentages_relative_to(cbis) + .auto_is(Length::zero); + if self.first_fragment { + ifc.inline_position += padding.inline_start + border.inline_start + margin.inline_start; + } else { + padding.inline_start = Length::zero(); + border.inline_start = Length::zero(); + margin.inline_start = Length::zero(); + } + let mut start_corner = Vec2 { + block: padding.block_start + border.block_start + margin.block_start, + inline: ifc.inline_position - ifc.current_nesting_level.inline_start, + }; + start_corner += &relative_adjustement( + &style, + ifc.containing_block.inline_size, + ifc.containing_block.block_size, + ); + PartialInlineBoxFragment { + style, + start_corner, + padding, + border, + margin, + last_box_tree_fragment: self.last_fragment, + parent_nesting_level: std::mem::replace( + &mut ifc.current_nesting_level, + InlineNestingLevelState { + remaining_boxes: self.children.iter(), + fragments_so_far: Vec::with_capacity(self.children.len()), + inline_start: ifc.inline_position, + max_block_size_of_fragments_so_far: Length::zero(), + }, + ), + } + } +} + +impl<'box_tree> PartialInlineBoxFragment<'box_tree> { + fn finish_layout( + &mut self, + nesting_level: &mut InlineNestingLevelState, + inline_position: &mut Length, + at_line_break: bool, + ) { + let mut fragment = BoxFragment { + style: self.style.clone(), + children: take(&mut nesting_level.fragments_so_far), + content_rect: Rect { + size: Vec2 { + inline: *inline_position - self.start_corner.inline, + block: nesting_level.max_block_size_of_fragments_so_far, + }, + start_corner: self.start_corner.clone(), + }, + padding: self.padding.clone(), + border: self.border.clone(), + margin: self.margin.clone(), + block_margins_collapsed_with_children: CollapsedBlockMargins::zero(), + }; + let last_fragment = self.last_box_tree_fragment && !at_line_break; + if last_fragment { + *inline_position += fragment.padding.inline_end + + fragment.border.inline_end + + fragment.margin.inline_end; + } else { + fragment.padding.inline_end = Length::zero(); + fragment.border.inline_end = Length::zero(); + fragment.margin.inline_end = Length::zero(); + } + self.parent_nesting_level + .max_block_size_of_fragments_so_far + .max_assign( + fragment.content_rect.size.block + + fragment.padding.block_sum() + + fragment.border.block_sum() + + fragment.margin.block_sum(), + ); + self.parent_nesting_level + .fragments_so_far + .push(Fragment::Box(fragment)); + } +} + +impl TextRun { + fn layout(&self, ifc: &mut InlineFormattingContextState) { + let available = ifc.containing_block.inline_size - ifc.inline_position; + let mut chars = self.text.chars(); + loop { + let mut shaped = ShapedSegment::new_with_naive_shaping(BITSTREAM_VERA_SANS.clone()); + let mut last_break_opportunity = None; + loop { + let next = chars.next(); + if matches!(next, Some(' ') | None) { + let inline_size = self.parent_style.font.font_size * shaped.advance_width; + if inline_size > available { + if let Some((state, iter)) = last_break_opportunity.take() { + shaped.restore(&state); + chars = iter; + } + break; + } + } + if let Some(ch) = next { + if ch == ' ' { + last_break_opportunity = Some((shaped.save(), chars.clone())) + } + shaped.append_char(ch).unwrap() + } else { + break; + } + } + let inline_size = self.parent_style.font.font_size * shaped.advance_width; + // https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + // 'normal': + // “set the used value to a "reasonable" value based on the font of the element.” + let line_height = self.parent_style.font.font_size.0 * 1.2; + let content_rect = Rect { + start_corner: Vec2 { + block: Length::zero(), + inline: ifc.inline_position - ifc.current_nesting_level.inline_start, + }, + size: Vec2 { + block: line_height, + inline: inline_size, + }, + }; + ifc.inline_position += inline_size; + ifc.current_nesting_level + .max_block_size_of_fragments_so_far + .max_assign(line_height); + ifc.current_nesting_level + .fragments_so_far + .push(Fragment::Text(TextFragment { + parent_style: self.parent_style.clone(), + content_rect, + text: shaped, + })); + if chars.as_str().is_empty() { + break; + } else { + // New line + ifc.current_nesting_level.inline_start = Length::zero(); + let mut nesting_level = &mut ifc.current_nesting_level; + for partial in ifc.partial_inline_boxes_stack.iter_mut().rev() { + partial.finish_layout(nesting_level, &mut ifc.inline_position, true); + partial.start_corner.inline = Length::zero(); + partial.padding.inline_start = Length::zero(); + partial.border.inline_start = Length::zero(); + partial.margin.inline_start = Length::zero(); + partial.parent_nesting_level.inline_start = Length::zero(); + nesting_level = &mut partial.parent_nesting_level; + } + ifc.line_boxes + .finish_line(nesting_level, ifc.containing_block); + ifc.inline_position = Length::zero(); + } + } + } +} diff --git a/components/layout_2020/flow/mod.rs b/components/layout_2020/flow/mod.rs new file mode 100644 index 00000000000..a810dfb37fa --- /dev/null +++ b/components/layout_2020/flow/mod.rs @@ -0,0 +1,439 @@ +//! Flow layout, also known as block-and-inline layout. + +use super::*; +use rayon::prelude::*; +use rayon_croissant::ParallelIteratorExt; + +mod construct; +mod float; +mod inline; +mod root; + +pub(super) use construct::*; +pub(super) use float::*; +pub(super) use inline::*; + +#[derive(Debug)] +pub(super) struct BlockFormattingContext { + pub contents: BlockContainer, + pub contains_floats: bool, +} + +#[derive(Debug)] +pub(super) enum BlockContainer { + BlockLevelBoxes(Vec<Arc<BlockLevelBox>>), + InlineFormattingContext(InlineFormattingContext), +} + +#[derive(Debug)] +pub(super) enum BlockLevelBox { + SameFormattingContextBlock { + style: Arc<ComputedValues>, + contents: BlockContainer, + }, + OutOfFlowAbsolutelyPositionedBox(AbsolutelyPositionedBox), + OutOfFlowFloatBox(FloatBox), + Independent { + style: Arc<ComputedValues>, + contents: IndependentFormattingContext, + }, +} + +pub(super) struct FlowChildren { + pub fragments: Vec<Fragment>, + pub block_size: Length, + pub collapsible_margins_in_children: CollapsedBlockMargins, +} + +#[derive(Clone, Copy)] +struct CollapsibleWithParentStartMargin(bool); + +impl BlockFormattingContext { + pub(super) fn layout<'a>( + &'a self, + containing_block: &ContainingBlock, + tree_rank: usize, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + ) -> FlowChildren { + let mut float_context; + let float_context = if self.contains_floats { + float_context = FloatContext::new(); + Some(&mut float_context) + } else { + None + }; + let mut flow_children = self.contents.layout( + containing_block, + tree_rank, + absolutely_positioned_fragments, + float_context, + CollapsibleWithParentStartMargin(false), + ); + flow_children.block_size += flow_children.collapsible_margins_in_children.end.solve(); + flow_children + .collapsible_margins_in_children + .collapsed_through = false; + flow_children.collapsible_margins_in_children.end = CollapsedMargin::zero(); + flow_children + } +} + +impl BlockContainer { + fn layout<'a>( + &'a self, + containing_block: &ContainingBlock, + tree_rank: usize, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + float_context: Option<&mut FloatContext>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, + ) -> FlowChildren { + match self { + BlockContainer::BlockLevelBoxes(child_boxes) => layout_block_level_children( + child_boxes, + containing_block, + tree_rank, + absolutely_positioned_fragments, + float_context, + collapsible_with_parent_start_margin, + ), + BlockContainer::InlineFormattingContext(ifc) => { + ifc.layout(containing_block, tree_rank, absolutely_positioned_fragments) + } + } + } +} + +fn layout_block_level_children<'a>( + child_boxes: &'a [Arc<BlockLevelBox>], + containing_block: &ContainingBlock, + tree_rank: usize, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + float_context: Option<&mut FloatContext>, + collapsible_with_parent_start_margin: CollapsibleWithParentStartMargin, +) -> FlowChildren { + fn place_block_level_fragment(fragment: &mut Fragment, placement_state: &mut PlacementState) { + match fragment { + Fragment::Box(fragment) => { + let fragment_block_margins = &fragment.block_margins_collapsed_with_children; + let fragment_block_size = fragment.padding.block_sum() + + fragment.border.block_sum() + + fragment.content_rect.size.block; + + if placement_state.next_in_flow_margin_collapses_with_parent_start_margin { + assert_eq!(placement_state.current_margin.solve(), Length::zero()); + placement_state + .start_margin + .adjoin_assign(&fragment_block_margins.start); + if fragment_block_margins.collapsed_through { + placement_state + .start_margin + .adjoin_assign(&fragment_block_margins.end); + return; + } + placement_state.next_in_flow_margin_collapses_with_parent_start_margin = false; + } else { + placement_state + .current_margin + .adjoin_assign(&fragment_block_margins.start); + } + fragment.content_rect.start_corner.block += placement_state.current_margin.solve() + + placement_state.current_block_direction_position; + if fragment_block_margins.collapsed_through { + placement_state + .current_margin + .adjoin_assign(&fragment_block_margins.end); + return; + } + placement_state.current_block_direction_position += + placement_state.current_margin.solve() + fragment_block_size; + placement_state.current_margin = fragment_block_margins.end; + } + Fragment::Anonymous(fragment) => { + // FIXME(nox): Margin collapsing for hypothetical boxes of + // abspos elements is probably wrong. + assert!(fragment.children.is_empty()); + assert_eq!(fragment.rect.size.block, Length::zero()); + fragment.rect.start_corner.block += + placement_state.current_block_direction_position; + } + _ => unreachable!(), + } + } + + struct PlacementState { + next_in_flow_margin_collapses_with_parent_start_margin: bool, + start_margin: CollapsedMargin, + current_margin: CollapsedMargin, + current_block_direction_position: Length, + } + + let abspos_so_far = absolutely_positioned_fragments.len(); + let mut placement_state = PlacementState { + next_in_flow_margin_collapses_with_parent_start_margin: + collapsible_with_parent_start_margin.0, + start_margin: CollapsedMargin::zero(), + current_margin: CollapsedMargin::zero(), + current_block_direction_position: Length::zero(), + }; + let mut fragments: Vec<_>; + if let Some(float_context) = float_context { + // Because floats are involved, we do layout for this block formatting context + // in tree order without parallelism. This enables mutable access + // to a `FloatContext` that tracks every float encountered so far (again in tree order). + fragments = child_boxes + .iter() + .enumerate() + .map(|(tree_rank, box_)| { + let mut fragment = box_.layout( + containing_block, + tree_rank, + absolutely_positioned_fragments, + Some(float_context), + ); + place_block_level_fragment(&mut fragment, &mut placement_state); + fragment + }) + .collect() + } else { + fragments = child_boxes + .par_iter() + .enumerate() + .mapfold_reduce_into( + absolutely_positioned_fragments, + |abspos_fragments, (tree_rank, box_)| { + box_.layout( + containing_block, + tree_rank, + abspos_fragments, + /* float_context = */ None, + ) + }, + |left_abspos_fragments, mut right_abspos_fragments| { + left_abspos_fragments.append(&mut right_abspos_fragments); + }, + ) + .collect(); + for fragment in &mut fragments { + place_block_level_fragment(fragment, &mut placement_state) + } + } + + adjust_static_positions( + &mut absolutely_positioned_fragments[abspos_so_far..], + &mut fragments, + tree_rank, + ); + + FlowChildren { + fragments, + block_size: placement_state.current_block_direction_position, + collapsible_margins_in_children: CollapsedBlockMargins { + collapsed_through: placement_state + .next_in_flow_margin_collapses_with_parent_start_margin, + start: placement_state.start_margin, + end: placement_state.current_margin, + }, + } +} + +impl BlockLevelBox { + fn layout<'a>( + &'a self, + containing_block: &ContainingBlock, + tree_rank: usize, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + float_context: Option<&mut FloatContext>, + ) -> Fragment { + match self { + BlockLevelBox::SameFormattingContextBlock { style, contents } => { + Fragment::Box(layout_in_flow_non_replaced_block_level( + containing_block, + absolutely_positioned_fragments, + style, + BlockLevelKind::SameFormattingContextBlock, + |containing_block, nested_abspos, collapsible_with_parent_start_margin| { + contents.layout( + containing_block, + tree_rank, + nested_abspos, + float_context, + collapsible_with_parent_start_margin, + ) + }, + )) + } + BlockLevelBox::Independent { style, contents } => match contents.as_replaced() { + Ok(replaced) => { + // FIXME + match *replaced {} + } + Err(contents) => Fragment::Box(layout_in_flow_non_replaced_block_level( + containing_block, + absolutely_positioned_fragments, + style, + BlockLevelKind::EstablishesAnIndependentFormattingContext, + |containing_block, nested_abspos, _| { + contents.layout(containing_block, tree_rank, nested_abspos) + }, + )), + }, + BlockLevelBox::OutOfFlowAbsolutelyPositionedBox(box_) => { + absolutely_positioned_fragments.push(box_.layout(Vec2::zero(), tree_rank)); + Fragment::Anonymous(AnonymousFragment::no_op(containing_block.mode)) + } + BlockLevelBox::OutOfFlowFloatBox(_box_) => { + // TODO + Fragment::Anonymous(AnonymousFragment::no_op(containing_block.mode)) + } + } + } +} + +#[derive(PartialEq)] +enum BlockLevelKind { + SameFormattingContextBlock, + EstablishesAnIndependentFormattingContext, +} + +/// https://drafts.csswg.org/css2/visudet.html#blockwidth +/// https://drafts.csswg.org/css2/visudet.html#normal-block +fn layout_in_flow_non_replaced_block_level<'a>( + containing_block: &ContainingBlock, + absolutely_positioned_fragments: &mut Vec<AbsolutelyPositionedFragment<'a>>, + style: &Arc<ComputedValues>, + block_level_kind: BlockLevelKind, + layout_contents: impl FnOnce( + &ContainingBlock, + &mut Vec<AbsolutelyPositionedFragment<'a>>, + CollapsibleWithParentStartMargin, + ) -> FlowChildren, +) -> BoxFragment { + let cbis = containing_block.inline_size; + let padding = style.padding().percentages_relative_to(cbis); + let border = style.border_width().percentages_relative_to(cbis); + let mut computed_margin = style.margin().percentages_relative_to(cbis); + let pb = &padding + &border; + let box_size = style.box_size(); + let inline_size = box_size.inline.percentage_relative_to(cbis); + if let LengthOrAuto::Length(is) = inline_size { + let inline_margins = cbis - is - pb.inline_sum(); + use LengthOrAuto::*; + match ( + &mut computed_margin.inline_start, + &mut computed_margin.inline_end, + ) { + (s @ &mut Auto, e @ &mut Auto) => { + *s = Length(inline_margins / 2.); + *e = Length(inline_margins / 2.); + } + (s @ &mut Auto, _) => { + *s = Length(inline_margins); + } + (_, e @ &mut Auto) => { + *e = Length(inline_margins); + } + (_, e @ _) => { + // Either the inline-end margin is auto, + // or we’re over-constrained and we do as if it were. + *e = Length(inline_margins); + } + } + } + let margin = computed_margin.auto_is(Length::zero); + let mut block_margins_collapsed_with_children = CollapsedBlockMargins::from_margin(&margin); + let inline_size = inline_size.auto_is(|| cbis - pb.inline_sum() - margin.inline_sum()); + let block_size = match box_size.block { + LengthOrPercentageOrAuto::Length(l) => LengthOrAuto::Length(l), + LengthOrPercentageOrAuto::Percentage(p) => containing_block.block_size.map(|cbbs| cbbs * p), + LengthOrPercentageOrAuto::Auto => LengthOrAuto::Auto, + }; + let containing_block_for_children = ContainingBlock { + inline_size, + block_size, + mode: style.writing_mode(), + }; + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + containing_block.mode, containing_block_for_children.mode, + "Mixed writing modes are not supported yet" + ); + let this_start_margin_can_collapse_with_children = CollapsibleWithParentStartMargin( + block_level_kind == BlockLevelKind::SameFormattingContextBlock + && pb.block_start == Length::zero(), + ); + let this_end_margin_can_collapse_with_children = (block_level_kind, pb.block_end, block_size) + == ( + BlockLevelKind::SameFormattingContextBlock, + Length::zero(), + LengthOrAuto::Auto, + ); + let mut nested_abspos = vec![]; + let mut flow_children = layout_contents( + &containing_block_for_children, + if style.box_.position.is_relatively_positioned() { + &mut nested_abspos + } else { + absolutely_positioned_fragments + }, + this_start_margin_can_collapse_with_children, + ); + if this_start_margin_can_collapse_with_children.0 { + block_margins_collapsed_with_children + .start + .adjoin_assign(&flow_children.collapsible_margins_in_children.start); + if flow_children + .collapsible_margins_in_children + .collapsed_through + { + block_margins_collapsed_with_children + .start + .adjoin_assign(&std::mem::replace( + &mut flow_children.collapsible_margins_in_children.end, + CollapsedMargin::zero(), + )); + } + } + if this_end_margin_can_collapse_with_children { + block_margins_collapsed_with_children + .end + .adjoin_assign(&flow_children.collapsible_margins_in_children.end); + } else { + flow_children.block_size += flow_children.collapsible_margins_in_children.end.solve(); + } + block_margins_collapsed_with_children.collapsed_through = + this_start_margin_can_collapse_with_children.0 + && this_end_margin_can_collapse_with_children + && flow_children + .collapsible_margins_in_children + .collapsed_through; + let relative_adjustement = relative_adjustement(style, inline_size, block_size); + let block_size = block_size.auto_is(|| flow_children.block_size); + let content_rect = Rect { + start_corner: Vec2 { + block: pb.block_start + relative_adjustement.block, + inline: pb.inline_start + relative_adjustement.inline + margin.inline_start, + }, + size: Vec2 { + block: block_size, + inline: inline_size, + }, + }; + if style.box_.position.is_relatively_positioned() { + AbsolutelyPositionedFragment::in_positioned_containing_block( + &nested_abspos, + &mut flow_children.fragments, + &content_rect.size, + &padding, + containing_block_for_children.mode, + ) + } + BoxFragment { + style: style.clone(), + children: flow_children.fragments, + content_rect, + padding, + border, + margin, + block_margins_collapsed_with_children, + } +} diff --git a/components/layout_2020/flow/root.rs b/components/layout_2020/flow/root.rs new file mode 100644 index 00000000000..443969c2acb --- /dev/null +++ b/components/layout_2020/flow/root.rs @@ -0,0 +1,122 @@ +use super::*; + +impl crate::dom::Document { + pub(crate) fn layout( + &self, + viewport: crate::primitives::Size<crate::primitives::CssPx>, + ) -> Vec<Fragment> { + BoxTreeRoot::construct(self).layout(viewport) + } +} + +struct BoxTreeRoot(BlockFormattingContext); + +impl BoxTreeRoot { + pub fn construct(document: &dom::Document) -> Self { + let author_styles = &document.parse_stylesheets(); + let context = Context { + document, + author_styles, + }; + let root_element = document.root_element(); + let style = style_for_element(context.author_styles, context.document, root_element, None); + let (contains_floats, boxes) = construct_for_root_element(&context, root_element, style); + Self(BlockFormattingContext { + contains_floats: contains_floats == ContainsFloats::Yes, + contents: BlockContainer::BlockLevelBoxes(boxes), + }) + } +} + +fn construct_for_root_element( + context: &Context, + root_element: dom::NodeId, + style: Arc<ComputedValues>, +) -> (ContainsFloats, Vec<Arc<BlockLevelBox>>) { + let replaced = ReplacedContent::for_element(root_element, context); + + let display_inside = match style.box_.display { + Display::None => return (ContainsFloats::No, Vec::new()), + Display::Contents if replaced.is_some() => { + // 'display: contents' computes to 'none' for replaced elements + return (ContainsFloats::No, Vec::new()); + } + // https://drafts.csswg.org/css-display-3/#transformations + Display::Contents => DisplayInside::Flow, + // The root element is blockified, ignore DisplayOutside + Display::GeneratingBox(DisplayGeneratingBox::OutsideInside { inside, .. }) => inside, + }; + + if let Some(replaced) = replaced { + let _box = match replaced {}; + #[allow(unreachable_code)] + { + return (ContainsFloats::No, vec![Arc::new(_box)]); + } + } + + let contents = IndependentFormattingContext::construct( + context, + &style, + display_inside, + Contents::OfElement(root_element), + ); + if style.box_.position.is_absolutely_positioned() { + ( + ContainsFloats::No, + vec![Arc::new(BlockLevelBox::OutOfFlowAbsolutelyPositionedBox( + AbsolutelyPositionedBox { style, contents }, + ))], + ) + } else if style.box_.float.is_floating() { + ( + ContainsFloats::Yes, + vec![Arc::new(BlockLevelBox::OutOfFlowFloatBox(FloatBox { + contents, + style, + }))], + ) + } else { + ( + ContainsFloats::No, + vec![Arc::new(BlockLevelBox::Independent { style, contents })], + ) + } +} + +impl BoxTreeRoot { + fn layout(&self, viewport: crate::primitives::Size<crate::primitives::CssPx>) -> Vec<Fragment> { + let initial_containing_block_size = Vec2 { + inline: Length { px: viewport.width }, + block: Length { + px: viewport.height, + }, + }; + + let initial_containing_block = ContainingBlock { + inline_size: initial_containing_block_size.inline, + block_size: LengthOrAuto::Length(initial_containing_block_size.block), + // FIXME: use the document’s mode: + // https://drafts.csswg.org/css-writing-modes/#principal-flow + mode: (WritingMode::HorizontalTb, Direction::Ltr), + }; + let dummy_tree_rank = 0; + let mut absolutely_positioned_fragments = vec![]; + let mut flow_children = self.0.layout( + &initial_containing_block, + dummy_tree_rank, + &mut absolutely_positioned_fragments, + ); + + let initial_containing_block = DefiniteContainingBlock { + size: initial_containing_block_size, + mode: initial_containing_block.mode, + }; + flow_children.fragments.par_extend( + absolutely_positioned_fragments + .par_iter() + .map(|a| a.layout(&initial_containing_block)), + ); + flow_children.fragments + } +} diff --git a/components/layout_2020/fragments.rs b/components/layout_2020/fragments.rs new file mode 100644 index 00000000000..f2a4dcd2986 --- /dev/null +++ b/components/layout_2020/fragments.rs @@ -0,0 +1,116 @@ +use super::*; +use crate::text::ShapedSegment; + +pub(crate) enum Fragment { + Box(BoxFragment), + Anonymous(AnonymousFragment), + Text(TextFragment), +} + +pub(crate) struct BoxFragment { + pub style: Arc<ComputedValues>, + pub children: Vec<Fragment>, + + /// From the containing block’s start corner…? + /// This might be broken when the containing block is in a different writing mode: + /// https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + pub content_rect: Rect<Length>, + + pub padding: Sides<Length>, + pub border: Sides<Length>, + pub margin: Sides<Length>, + + pub block_margins_collapsed_with_children: CollapsedBlockMargins, +} + +pub(crate) struct CollapsedBlockMargins { + pub collapsed_through: bool, + pub start: CollapsedMargin, + pub end: CollapsedMargin, +} + +#[derive(Clone, Copy)] +pub(crate) struct CollapsedMargin { + max_positive: Length, + min_negative: Length, +} + +/// Can contain child fragments with relative coordinates, but does not contribute to painting itself. +pub(crate) struct AnonymousFragment { + pub rect: Rect<Length>, + pub children: Vec<Fragment>, + pub mode: (WritingMode, Direction), +} + +pub(crate) struct TextFragment { + pub parent_style: Arc<ComputedValues>, + pub content_rect: Rect<Length>, + pub text: ShapedSegment, +} + +impl AnonymousFragment { + pub fn no_op(mode: (WritingMode, Direction)) -> Self { + Self { + children: vec![], + rect: Rect::zero(), + mode, + } + } +} + +impl BoxFragment { + pub fn border_rect(&self) -> Rect<Length> { + self.content_rect + .inflate(&self.padding) + .inflate(&self.border) + } +} + +impl CollapsedBlockMargins { + pub fn from_margin(margin: &Sides<Length>) -> Self { + Self { + collapsed_through: false, + start: CollapsedMargin::new(margin.block_start), + end: CollapsedMargin::new(margin.block_end), + } + } + + pub fn zero() -> Self { + Self { + collapsed_through: false, + start: CollapsedMargin::zero(), + end: CollapsedMargin::zero(), + } + } +} + +impl CollapsedMargin { + pub fn zero() -> Self { + Self { + max_positive: Length::zero(), + min_negative: Length::zero(), + } + } + + pub fn new(margin: Length) -> Self { + Self { + max_positive: margin.max(Length::zero()), + min_negative: margin.min(Length::zero()), + } + } + + pub fn adjoin(&self, other: &Self) -> Self { + Self { + max_positive: self.max_positive.max(other.max_positive), + min_negative: self.min_negative.min(other.min_negative), + } + } + + pub fn adjoin_assign(&mut self, other: &Self) { + *self = self.adjoin(other); + } + + pub fn solve(&self) -> Length { + self.max_positive + self.min_negative + } +} diff --git a/components/layout_2020/geom.rs b/components/layout_2020/geom.rs new file mode 100644 index 00000000000..2032a3e72ab --- /dev/null +++ b/components/layout_2020/geom.rs @@ -0,0 +1,330 @@ +pub(crate) use crate::style::values::Length; +use crate::style::values::{LengthOrAuto, LengthOrPercentage, LengthOrPercentageOrAuto}; + +pub(crate) mod physical { + #[derive(Debug, Clone)] + pub(crate) struct Vec2<T> { + pub x: T, + pub y: T, + } + + #[derive(Debug, Clone)] + pub(crate) struct Rect<T> { + pub top_left: Vec2<T>, + pub size: Vec2<T>, + } + + #[derive(Debug, Clone)] + pub(crate) struct Sides<T> { + pub top: T, + pub left: T, + pub bottom: T, + pub right: T, + } +} + +pub(crate) mod flow_relative { + #[derive(Debug, Clone)] + pub(crate) struct Vec2<T> { + pub inline: T, + pub block: T, + } + + #[derive(Debug, Clone)] + pub(crate) struct Rect<T> { + pub start_corner: Vec2<T>, + pub size: Vec2<T>, + } + + #[derive(Debug, Clone)] + pub(crate) struct Sides<T> { + pub inline_start: T, + pub inline_end: T, + pub block_start: T, + pub block_end: T, + } +} + +use crate::style::values::{Direction, WritingMode}; +use std::ops::{Add, AddAssign, Sub}; + +impl<T> Add<&'_ physical::Vec2<T>> for &'_ physical::Vec2<T> +where + T: Add<Output = T> + Copy, +{ + type Output = physical::Vec2<T>; + + fn add(self, other: &'_ physical::Vec2<T>) -> Self::Output { + physical::Vec2 { + x: self.x + other.x, + y: self.y + other.y, + } + } +} + +impl<T: Clone> physical::Vec2<T> { + pub fn size_to_flow_relative(&self, mode: (WritingMode, Direction)) -> flow_relative::Vec2<T> { + // https://drafts.csswg.org/css-writing-modes/#logical-to-physical + let (i, b) = if let (WritingMode::HorizontalTb, _) = mode { + (&self.x, &self.y) + } else { + (&self.y, &self.x) + }; + flow_relative::Vec2 { + inline: i.clone(), + block: b.clone(), + } + } +} + +impl<T> Add<&'_ flow_relative::Vec2<T>> for &'_ flow_relative::Vec2<T> +where + T: Add<Output = T> + Copy, +{ + type Output = flow_relative::Vec2<T>; + + fn add(self, other: &'_ flow_relative::Vec2<T>) -> Self::Output { + flow_relative::Vec2 { + inline: self.inline + other.inline, + block: self.block + other.block, + } + } +} + +impl<T> AddAssign<&'_ flow_relative::Vec2<T>> for flow_relative::Vec2<T> +where + T: AddAssign<T> + Copy, +{ + fn add_assign(&mut self, other: &'_ flow_relative::Vec2<T>) { + self.inline += other.inline; + self.block += other.block; + } +} + +impl flow_relative::Vec2<Length> { + pub fn zero() -> Self { + Self { + inline: Length::zero(), + block: Length::zero(), + } + } +} + +impl flow_relative::Sides<Length> { + pub fn zero() -> Self { + Self { + inline_start: Length::zero(), + inline_end: Length::zero(), + block_start: Length::zero(), + block_end: Length::zero(), + } + } +} + +impl flow_relative::Rect<Length> { + pub fn zero() -> Self { + Self { + start_corner: flow_relative::Vec2::zero(), + size: flow_relative::Vec2::zero(), + } + } +} + +impl<T: Clone> flow_relative::Vec2<T> { + pub fn size_to_physical(&self, mode: (WritingMode, Direction)) -> physical::Vec2<T> { + // https://drafts.csswg.org/css-writing-modes/#logical-to-physical + let (x, y) = if let (WritingMode::HorizontalTb, _) = mode { + (&self.inline, &self.block) + } else { + (&self.block, &self.inline) + }; + physical::Vec2 { + x: x.clone(), + y: y.clone(), + } + } +} + +impl From<physical::Vec2<Length>> for crate::primitives::Point<crate::primitives::CssPx> { + fn from(v: physical::Vec2<Length>) -> Self { + crate::primitives::Point::from_lengths(v.x.into(), v.y.into()) + } +} + +impl<T: Clone> physical::Sides<T> { + pub fn to_flow_relative(&self, mode: (WritingMode, Direction)) -> flow_relative::Sides<T> { + use Direction::*; + use WritingMode::*; + + // https://drafts.csswg.org/css-writing-modes/#logical-to-physical + let (bs, be) = match mode.0 { + HorizontalTb => (&self.top, &self.bottom), + VerticalRl | SidewaysRl => (&self.right, &self.left), + VerticalLr | SidewaysLr => (&self.left, &self.right), + }; + let (is, ie) = match mode { + (HorizontalTb, Ltr) => (&self.left, &self.right), + (HorizontalTb, Rtl) => (&self.right, &self.left), + (VerticalRl, Ltr) | (SidewaysRl, Ltr) | (VerticalLr, Ltr) | (SidewaysLr, Rtl) => { + (&self.top, &self.bottom) + } + (VerticalRl, Rtl) | (SidewaysRl, Rtl) | (VerticalLr, Rtl) | (SidewaysLr, Ltr) => { + (&self.bottom, &self.top) + } + }; + flow_relative::Sides { + inline_start: is.clone(), + inline_end: ie.clone(), + block_start: bs.clone(), + block_end: be.clone(), + } + } +} + +impl<T> flow_relative::Sides<T> { + pub fn map<U>(&self, f: impl Fn(&T) -> U) -> flow_relative::Sides<U> { + flow_relative::Sides { + inline_start: f(&self.inline_start), + inline_end: f(&self.inline_end), + block_start: f(&self.block_start), + block_end: f(&self.block_end), + } + } + + pub fn map_inline_and_block_axes<U>( + &self, + inline_f: impl Fn(&T) -> U, + block_f: impl Fn(&T) -> U, + ) -> flow_relative::Sides<U> { + flow_relative::Sides { + inline_start: inline_f(&self.inline_start), + inline_end: inline_f(&self.inline_end), + block_start: block_f(&self.block_start), + block_end: block_f(&self.block_end), + } + } + + pub fn inline_sum(&self) -> T::Output + where + T: Add + Copy, + { + self.inline_start + self.inline_end + } + + pub fn block_sum(&self) -> T::Output + where + T: Add + Copy, + { + self.block_start + self.block_end + } + + pub fn start_corner(&self) -> flow_relative::Vec2<T> + where + T: Clone, + { + flow_relative::Vec2 { + inline: self.inline_start.clone(), + block: self.block_start.clone(), + } + } +} + +impl flow_relative::Sides<LengthOrPercentage> { + pub fn percentages_relative_to(&self, basis: Length) -> flow_relative::Sides<Length> { + self.map(|s| s.percentage_relative_to(basis)) + } +} + +impl flow_relative::Sides<LengthOrPercentageOrAuto> { + pub fn percentages_relative_to(&self, basis: Length) -> flow_relative::Sides<LengthOrAuto> { + self.map(|s| s.percentage_relative_to(basis)) + } +} + +impl flow_relative::Sides<LengthOrAuto> { + pub fn auto_is(&self, f: impl Fn() -> Length) -> flow_relative::Sides<Length> { + self.map(|s| s.auto_is(&f)) + } +} + +impl<T> Add<&'_ flow_relative::Sides<T>> for &'_ flow_relative::Sides<T> +where + T: Add<Output = T> + Copy, +{ + type Output = flow_relative::Sides<T>; + + fn add(self, other: &'_ flow_relative::Sides<T>) -> Self::Output { + flow_relative::Sides { + inline_start: self.inline_start + other.inline_start, + inline_end: self.inline_end + other.inline_end, + block_start: self.block_start + other.block_start, + block_end: self.block_end + other.block_end, + } + } +} + +impl<T> flow_relative::Rect<T> { + pub fn inflate(&self, sides: &flow_relative::Sides<T>) -> Self + where + T: Add<Output = T> + Copy, + T: Sub<Output = T> + Copy, + { + flow_relative::Rect { + start_corner: flow_relative::Vec2 { + inline: self.start_corner.inline - sides.inline_start, + block: self.start_corner.block - sides.block_start, + }, + size: flow_relative::Vec2 { + inline: self.size.inline + sides.inline_sum(), + block: self.size.block + sides.block_sum(), + }, + } + } + + pub fn to_physical( + &self, + mode: (WritingMode, Direction), + // Will be needed for other writing modes + // FIXME: what if the containing block has a different mode? + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + _containing_block: &physical::Rect<T>, + ) -> physical::Rect<T> + where + T: Clone, + { + // Top-left corner + let (tl_x, tl_y) = if let (WritingMode::HorizontalTb, Direction::Ltr) = mode { + (&self.start_corner.inline, &self.start_corner.block) + } else { + unimplemented!() + }; + physical::Rect { + top_left: physical::Vec2 { + x: tl_x.clone(), + y: tl_y.clone(), + }, + size: self.size.size_to_physical(mode), + } + } +} + +impl<T> physical::Rect<T> { + pub fn translate(&self, by: &physical::Vec2<T>) -> Self + where + T: Add<Output = T> + Copy, + { + physical::Rect { + top_left: &self.top_left + by, + size: self.size.clone(), + } + } +} + +impl From<physical::Rect<Length>> for crate::primitives::Rect<crate::primitives::CssPx> { + fn from(r: physical::Rect<Length>) -> Self { + crate::primitives::Rect { + origin: crate::primitives::Point::new(r.top_left.x.px, r.top_left.y.px), + size: crate::primitives::Size::new(r.size.x.px, r.size.y.px), + } + } +} diff --git a/components/layout_2020/positioned.rs b/components/layout_2020/positioned.rs new file mode 100644 index 00000000000..9794d1acdae --- /dev/null +++ b/components/layout_2020/positioned.rs @@ -0,0 +1,328 @@ +use super::*; +use rayon::prelude::*; + +#[derive(Debug)] +pub(super) struct AbsolutelyPositionedBox { + pub style: Arc<ComputedValues>, + pub contents: IndependentFormattingContext, +} + +#[derive(Debug)] +pub(super) struct AbsolutelyPositionedFragment<'box_> { + absolutely_positioned_box: &'box_ AbsolutelyPositionedBox, + + /// The rank of the child from which this absolutely positioned fragment + /// came from, when doing the layout of a block container. Used to compute + /// static positions when going up the tree. + pub(super) tree_rank: usize, + + pub(super) inline_start: AbsoluteBoxOffsets<LengthOrPercentage>, + inline_size: LengthOrPercentageOrAuto, + + pub(super) block_start: AbsoluteBoxOffsets<LengthOrPercentage>, + block_size: LengthOrPercentageOrAuto, +} + +#[derive(Clone, Copy, Debug)] +pub(super) enum AbsoluteBoxOffsets<NonStatic> { + StaticStart { start: Length }, + Start { start: NonStatic }, + End { end: NonStatic }, + Both { start: NonStatic, end: NonStatic }, +} + +impl AbsolutelyPositionedBox { + pub(super) fn layout<'a>( + &'a self, + initial_start_corner: Vec2<Length>, + tree_rank: usize, + ) -> AbsolutelyPositionedFragment { + let style = &self.style; + let box_offsets = style.box_offsets(); + let box_size = style.box_size(); + + let inline_size = box_size.inline; + let block_size = box_size.block; + + fn absolute_box_offsets( + initial_static_start: Length, + start: LengthOrPercentageOrAuto, + end: LengthOrPercentageOrAuto, + ) -> AbsoluteBoxOffsets<LengthOrPercentage> { + match (start.non_auto(), end.non_auto()) { + (None, None) => AbsoluteBoxOffsets::StaticStart { + start: initial_static_start, + }, + (Some(start), Some(end)) => AbsoluteBoxOffsets::Both { start, end }, + (None, Some(end)) => AbsoluteBoxOffsets::End { end }, + (Some(start), None) => AbsoluteBoxOffsets::Start { start }, + } + } + + let inline_start = absolute_box_offsets( + initial_start_corner.inline, + box_offsets.inline_start, + box_offsets.inline_end, + ); + let block_start = absolute_box_offsets( + initial_start_corner.block, + box_offsets.block_start, + box_offsets.block_end, + ); + + AbsolutelyPositionedFragment { + absolutely_positioned_box: self, + tree_rank, + inline_start, + inline_size, + block_start, + block_size, + } + } +} + +impl<'a> AbsolutelyPositionedFragment<'a> { + pub(super) fn in_positioned_containing_block( + absolute: &[Self], + fragments: &mut Vec<Fragment>, + content_rect_size: &Vec2<Length>, + padding: &Sides<Length>, + mode: (WritingMode, Direction), + ) { + if absolute.is_empty() { + return; + } + let padding_rect = Rect { + size: content_rect_size.clone(), + // Ignore the content rect’s position in its own containing block: + start_corner: Vec2::zero(), + } + .inflate(&padding); + let containing_block = DefiniteContainingBlock { + size: padding_rect.size.clone(), + mode, + }; + fragments.push(Fragment::Anonymous(AnonymousFragment { + children: absolute + .par_iter() + .map(|a| a.layout(&containing_block)) + .collect(), + rect: padding_rect, + mode, + })) + } + + pub(super) fn layout(&self, containing_block: &DefiniteContainingBlock) -> Fragment { + let style = &self.absolutely_positioned_box.style; + let cbis = containing_block.size.inline; + let cbbs = containing_block.size.block; + + let padding = style.padding().percentages_relative_to(cbis); + let border = style.border_width().percentages_relative_to(cbis); + let computed_margin = style.margin().percentages_relative_to(cbis); + let pb = &padding + &border; + + enum Anchor { + Start(Length), + End(Length), + } + + fn solve_axis( + containing_size: Length, + padding_border_sum: Length, + computed_margin_start: LengthOrAuto, + computed_margin_end: LengthOrAuto, + solve_margins: impl FnOnce(Length) -> (Length, Length), + box_offsets: AbsoluteBoxOffsets<LengthOrPercentage>, + size: LengthOrPercentageOrAuto, + ) -> (Anchor, LengthOrAuto, Length, Length) { + let size = size.percentage_relative_to(containing_size); + match box_offsets { + AbsoluteBoxOffsets::StaticStart { start } => ( + Anchor::Start(start), + size, + computed_margin_start.auto_is(Length::zero), + computed_margin_end.auto_is(Length::zero), + ), + AbsoluteBoxOffsets::Start { start } => ( + Anchor::Start(start.percentage_relative_to(containing_size)), + size, + computed_margin_start.auto_is(Length::zero), + computed_margin_end.auto_is(Length::zero), + ), + AbsoluteBoxOffsets::End { end } => ( + Anchor::End(end.percentage_relative_to(containing_size)), + size, + computed_margin_start.auto_is(Length::zero), + computed_margin_end.auto_is(Length::zero), + ), + AbsoluteBoxOffsets::Both { start, end } => { + let start = start.percentage_relative_to(containing_size); + let end = end.percentage_relative_to(containing_size); + + let mut margin_start = computed_margin_start.auto_is(Length::zero); + let mut margin_end = computed_margin_end.auto_is(Length::zero); + + let size = if let LengthOrAuto::Length(size) = size { + use LengthOrAuto::Auto; + let margins = containing_size - start - end - padding_border_sum - size; + match (computed_margin_start, computed_margin_end) { + (Auto, Auto) => { + let (s, e) = solve_margins(margins); + margin_start = s; + margin_end = e; + } + (Auto, LengthOrAuto::Length(end)) => { + margin_start = margins - end; + } + (LengthOrAuto::Length(start), Auto) => { + margin_end = margins - start; + } + (LengthOrAuto::Length(_), LengthOrAuto::Length(_)) => {} + } + size + } else { + // FIXME(nox): What happens if that is negative? + containing_size + - start + - end + - padding_border_sum + - margin_start + - margin_end + }; + ( + Anchor::Start(start), + LengthOrAuto::Length(size), + margin_start, + margin_end, + ) + } + } + } + + let (inline_anchor, inline_size, margin_inline_start, margin_inline_end) = solve_axis( + cbis, + pb.inline_sum(), + computed_margin.inline_start, + computed_margin.inline_end, + |margins| { + if margins.px >= 0. { + (margins / 2., margins / 2.) + } else { + (Length::zero(), margins) + } + }, + self.inline_start, + self.inline_size, + ); + + let (block_anchor, block_size, margin_block_start, margin_block_end) = solve_axis( + cbis, + pb.block_sum(), + computed_margin.block_start, + computed_margin.block_end, + |margins| (margins / 2., margins / 2.), + self.block_start, + self.block_size, + ); + + let margin = Sides { + inline_start: margin_inline_start, + inline_end: margin_inline_end, + block_start: margin_block_start, + block_end: margin_block_end, + }; + + let inline_size = inline_size.auto_is(|| { + let available_size = match inline_anchor { + Anchor::Start(start) => cbis - start - pb.inline_sum() - margin.inline_sum(), + Anchor::End(end) => cbis - end - pb.inline_sum() - margin.inline_sum(), + }; + + // FIXME(nox): shrink-to-fit. + available_size + }); + + let containing_block_for_children = ContainingBlock { + inline_size, + block_size, + mode: style.writing_mode(), + }; + // https://drafts.csswg.org/css-writing-modes/#orthogonal-flows + assert_eq!( + containing_block.mode, containing_block_for_children.mode, + "Mixed writing modes are not supported yet" + ); + let dummy_tree_rank = 0; + let mut absolutely_positioned_fragments = vec![]; + let mut flow_children = self.absolutely_positioned_box.contents.layout( + &containing_block_for_children, + dummy_tree_rank, + &mut absolutely_positioned_fragments, + ); + + let inline_start = match inline_anchor { + Anchor::Start(start) => start + pb.inline_start + margin.inline_start, + Anchor::End(end) => cbbs - end - pb.inline_end - margin.inline_end - inline_size, + }; + + let block_size = block_size.auto_is(|| flow_children.block_size); + let block_start = match block_anchor { + Anchor::Start(start) => start + pb.block_start + margin.block_start, + Anchor::End(end) => cbbs - end - pb.block_end - margin.block_end - block_size, + }; + + let content_rect = Rect { + start_corner: Vec2 { + inline: inline_start, + block: block_start, + }, + size: Vec2 { + inline: inline_size, + block: block_size, + }, + }; + + AbsolutelyPositionedFragment::in_positioned_containing_block( + &absolutely_positioned_fragments, + &mut flow_children.fragments, + &content_rect.size, + &padding, + containing_block_for_children.mode, + ); + + Fragment::Box(BoxFragment { + style: style.clone(), + children: flow_children.fragments, + content_rect, + padding, + border, + margin, + block_margins_collapsed_with_children: CollapsedBlockMargins::zero(), + }) + } +} + +pub(super) fn adjust_static_positions( + absolutely_positioned_fragments: &mut [AbsolutelyPositionedFragment], + child_fragments: &mut [Fragment], + tree_rank_in_parent: usize, +) { + for abspos_fragment in absolutely_positioned_fragments { + let child_fragment_rect = match &child_fragments[abspos_fragment.tree_rank] { + Fragment::Box(b) => &b.content_rect, + Fragment::Anonymous(a) => &a.rect, + _ => unreachable!(), + }; + + abspos_fragment.tree_rank = tree_rank_in_parent; + + if let AbsoluteBoxOffsets::StaticStart { start } = &mut abspos_fragment.inline_start { + *start += child_fragment_rect.start_corner.inline; + } + + if let AbsoluteBoxOffsets::StaticStart { start } = &mut abspos_fragment.block_start { + *start += child_fragment_rect.start_corner.block; + } + } +} diff --git a/components/layout_2020/primitives.rs b/components/layout_2020/primitives.rs new file mode 100644 index 00000000000..0991478ac06 --- /dev/null +++ b/components/layout_2020/primitives.rs @@ -0,0 +1,28 @@ +use crate::text; + +/// Origin at top-left corner, unit `1px` +pub struct CssPx; + +pub use euclid::point2 as point; +pub use euclid::rect; +pub type Length<U> = euclid::Length<f32, U>; +pub type Point<U> = euclid::TypedPoint2D<f32, U>; +pub type Size<U> = euclid::TypedSize2D<f32, U>; +pub type Rect<U> = euclid::TypedRect<f32, U>; +pub type SideOffsets<U> = euclid::TypedSideOffsets2D<f32, U>; +pub type Scale<Src, Dest> = euclid::TypedScale<f32, Src, Dest>; + +#[derive(Copy, Clone, PartialEq)] +pub struct RGBA(pub f32, pub f32, pub f32, pub f32); + +pub struct TextRun<'a> { + pub segment: &'a text::ShapedSegment, + pub font_size: Length<CssPx>, + pub origin: Point<CssPx>, +} + +impl From<cssparser::RGBA> for RGBA { + fn from(c: cssparser::RGBA) -> Self { + RGBA(c.red_f32(), c.green_f32(), c.blue_f32(), c.alpha_f32()) + } +} diff --git a/components/layout_2020/replaced.rs b/components/layout_2020/replaced.rs new file mode 100644 index 00000000000..312326c84af --- /dev/null +++ b/components/layout_2020/replaced.rs @@ -0,0 +1,14 @@ +use super::*; +use crate::dom::NodeId; + +#[derive(Debug)] +pub(super) enum ReplacedContent { + // Not implemented yet +} + +impl ReplacedContent { + pub fn for_element(_element: NodeId, _context: &Context) -> Option<Self> { + // FIXME: implement <img> etc. + None + } +} |