layout: Implement overflow-wrap/word-wrap per CSS-TEXT § 6.2.

This property is used by approximately 55% of page loads.

To implement the line breaking behavior, the "breaking strategy" has
been cleaned up and abstracted. This should allow us to easily support
other similar properties in the future, such as `text-overflow` and
`word-break`.
This commit is contained in:
Patrick Walton 2014-12-12 22:03:00 -08:00
parent 1be7d7cced
commit 6943ddb93e
8 changed files with 356 additions and 139 deletions

View file

@ -855,8 +855,8 @@ impl ScaledFontExtensionMethods for ScaledFont {
let mut azglyphs = vec!();
azglyphs.reserve(range.length().to_uint());
for (glyphs, _offset, slice_range) in run.iter_slices_for_range(range) {
for (_i, glyph) in glyphs.iter_glyphs_for_char_range(&slice_range) {
for slice in run.natural_word_slices_in_range(range) {
for (_i, glyph) in slice.glyphs.iter_glyphs_for_char_range(&slice.range) {
let glyph_advance = glyph.advance();
let glyph_offset = glyph.offset().unwrap_or(Zero::zero());
let azglyph = struct__AzGlyph {

View file

@ -27,14 +27,14 @@ pub struct TextRun {
#[deriving(Clone)]
pub struct GlyphRun {
/// The glyphs.
glyph_store: Arc<GlyphStore>,
pub glyph_store: Arc<GlyphStore>,
/// The range of characters in the containing run.
range: Range<CharIndex>,
pub range: Range<CharIndex>,
}
pub struct SliceIterator<'a> {
pub struct NaturalWordSliceIterator<'a> {
glyph_iter: Items<'a, GlyphRun>,
range: Range<CharIndex>,
range: Range<CharIndex>,
}
struct CharIndexComparator;
@ -51,10 +51,31 @@ impl Comparator<CharIndex,GlyphRun> for CharIndexComparator {
}
}
impl<'a> Iterator<(&'a GlyphStore, CharIndex, Range<CharIndex>)> for SliceIterator<'a> {
/// A "slice" of a text run is a series of contiguous glyphs that all belong to the same glyph
/// store. Line breaking strategies yield these.
pub struct TextRunSlice<'a> {
/// The glyph store that the glyphs in this slice belong to.
pub glyphs: &'a GlyphStore,
/// The character index that this slice begins at, relative to the start of the *text run*.
pub offset: CharIndex,
/// The range that these glyphs encompass, relative to the start of the *glyph store*.
pub range: Range<CharIndex>,
}
impl<'a> TextRunSlice<'a> {
/// Returns the range that these glyphs encompass, relative to the start of the *text run*.
#[inline]
pub fn text_run_range(&self) -> Range<CharIndex> {
let mut range = self.range;
range.shift_by(self.offset);
range
}
}
impl<'a> Iterator<TextRunSlice<'a>> for NaturalWordSliceIterator<'a> {
// inline(always) due to the inefficient rt failures messing up inline heuristics, I think.
#[inline(always)]
fn next(&mut self) -> Option<(&'a GlyphStore, CharIndex, Range<CharIndex>)> {
fn next(&mut self) -> Option<TextRunSlice<'a>> {
let slice_glyphs = self.glyph_iter.next();
if slice_glyphs.is_none() {
return None;
@ -64,18 +85,58 @@ impl<'a> Iterator<(&'a GlyphStore, CharIndex, Range<CharIndex>)> for SliceIterat
let mut char_range = self.range.intersect(&slice_glyphs.range);
let slice_range_begin = slice_glyphs.range.begin();
char_range.shift_by(-slice_range_begin);
if !char_range.is_empty() {
return Some((&*slice_glyphs.glyph_store, slice_range_begin, char_range))
Some(TextRunSlice {
glyphs: &*slice_glyphs.glyph_store,
offset: slice_range_begin,
range: char_range,
})
} else {
None
}
}
}
pub struct CharacterSliceIterator<'a> {
glyph_run: Option<&'a GlyphRun>,
glyph_run_iter: Items<'a, GlyphRun>,
range: Range<CharIndex>,
}
impl<'a> Iterator<TextRunSlice<'a>> for CharacterSliceIterator<'a> {
// inline(always) due to the inefficient rt failures messing up inline heuristics, I think.
#[inline(always)]
fn next(&mut self) -> Option<TextRunSlice<'a>> {
let glyph_run = match self.glyph_run {
None => return None,
Some(glyph_run) => glyph_run,
};
debug_assert!(!self.range.is_empty());
let index_to_return = self.range.begin();
self.range.adjust_by(CharIndex(1), CharIndex(0));
if self.range.is_empty() {
// We're done.
self.glyph_run = None
} else if self.range.intersect(&glyph_run.range).is_empty() {
// Move on to the next glyph run.
self.glyph_run = self.glyph_run_iter.next();
}
return None;
let index_within_glyph_run = index_to_return - glyph_run.range.begin();
Some(TextRunSlice {
glyphs: &*glyph_run.glyph_store,
offset: glyph_run.range.begin(),
range: Range::new(index_within_glyph_run, CharIndex(1)),
})
}
}
pub struct LineIterator<'a> {
range: Range<CharIndex>,
clump: Option<Range<CharIndex>>,
slices: SliceIterator<'a>,
range: Range<CharIndex>,
clump: Option<Range<CharIndex>>,
slices: NaturalWordSliceIterator<'a>,
}
impl<'a> Iterator<Range<CharIndex>> for LineIterator<'a> {
@ -83,30 +144,30 @@ impl<'a> Iterator<Range<CharIndex>> for LineIterator<'a> {
// Loop until we hit whitespace and are in a clump.
loop {
match self.slices.next() {
Some((glyphs, offset, slice_range)) => {
match (glyphs.is_whitespace(), self.clump) {
Some(slice) => {
match (slice.glyphs.is_whitespace(), self.clump) {
(false, Some(ref mut c)) => {
c.extend_by(slice_range.length());
c.extend_by(slice.range.length());
}
(false, None) => {
let mut c = slice_range;
c.shift_by(offset);
self.clump = Some(c);
let mut range = slice.range;
range.shift_by(slice.offset);
self.clump = Some(range);
}
(true, None) => { /* chomp whitespace */ }
(true, Some(c)) => {
(true, Some(clump)) => {
self.clump = None;
// The final whitespace clump is not included.
return Some(c);
return Some(clump);
}
}
},
}
None => {
// flush any remaining chars as a line
// Flush any remaining characters as a line.
if self.clump.is_some() {
let mut c = self.clump.take().unwrap();
c.extend_to(self.range.end());
return Some(c);
let mut range = self.clump.take().unwrap();
range.extend_to(self.range.end());
return Some(range);
} else {
return None;
}
@ -216,9 +277,7 @@ impl<'a> TextRun {
}
pub fn range_is_trimmable_whitespace(&self, range: &Range<CharIndex>) -> bool {
self.iter_slices_for_range(range).all(|(slice_glyphs, _, _)| {
slice_glyphs.is_whitespace()
})
self.natural_word_slices_in_range(range).all(|slice| slice.glyphs.is_whitespace())
}
pub fn ascent(&self) -> Au {
@ -232,9 +291,9 @@ impl<'a> TextRun {
pub fn advance_for_range(&self, range: &Range<CharIndex>) -> Au {
// TODO(Issue #199): alter advance direction for RTL
// TODO(Issue #98): using inter-char and inter-word spacing settings when measuring text
self.iter_slices_for_range(range)
.fold(Au(0), |advance, (glyphs, _, slice_range)| {
advance + glyphs.advance_for_char_range(&slice_range)
self.natural_word_slices_in_range(range)
.fold(Au(0), |advance, slice| {
advance + slice.glyphs.advance_for_char_range(&slice.range)
})
}
@ -252,33 +311,58 @@ impl<'a> TextRun {
pub fn min_width_for_range(&self, range: &Range<CharIndex>) -> Au {
debug!("iterating outer range {}", range);
self.iter_slices_for_range(range).fold(Au(0), |max_piece_width, (_, offset, slice_range)| {
debug!("iterated on {}[{}]", offset, slice_range);
Au::max(max_piece_width, self.advance_for_range(&slice_range))
self.natural_word_slices_in_range(range).fold(Au(0), |max_piece_width, slice| {
debug!("iterated on {}[{}]", slice.offset, slice.range);
Au::max(max_piece_width, self.advance_for_range(&slice.range))
})
}
/// Returns the first glyph run containing the given character index.
pub fn first_glyph_run_containing(&'a self, index: CharIndex) -> Option<&'a GlyphRun> {
self.index_of_first_glyph_run_containing(index).map(|index| &self.glyphs[index])
}
/// Returns the index of the first glyph run containing the given character index.
fn index_of_first_glyph_run_containing(&self, index: CharIndex) -> Option<uint> {
self.glyphs.as_slice().binary_search_index_by(&index, CharIndexComparator)
}
pub fn iter_slices_for_range(&'a self, range: &Range<CharIndex>) -> SliceIterator<'a> {
/// Returns an iterator that will iterate over all slices of glyphs that represent natural
/// words in the given range.
pub fn natural_word_slices_in_range(&'a self, range: &Range<CharIndex>)
-> NaturalWordSliceIterator<'a> {
let index = match self.index_of_first_glyph_run_containing(range.begin()) {
None => self.glyphs.len(),
Some(index) => index,
};
SliceIterator {
NaturalWordSliceIterator {
glyph_iter: self.glyphs.slice_from(index).iter(),
range: *range,
range: *range,
}
}
/// Returns an iterator that will iterate over all slices of glyphs that represent individual
/// characters in the given range.
pub fn character_slices_in_range(&'a self, range: &Range<CharIndex>)
-> CharacterSliceIterator<'a> {
let index = match self.index_of_first_glyph_run_containing(range.begin()) {
None => self.glyphs.len(),
Some(index) => index,
};
let mut glyph_run_iter = self.glyphs.slice_from(index).iter();
let first_glyph_run = glyph_run_iter.next();
CharacterSliceIterator {
glyph_run: first_glyph_run,
glyph_run_iter: glyph_run_iter,
range: *range,
}
}
pub fn iter_natural_lines_for_range(&'a self, range: &Range<CharIndex>) -> LineIterator<'a> {
LineIterator {
range: *range,
clump: None,
slices: self.iter_slices_for_range(range),
range: *range,
clump: None,
slices: self.natural_word_slices_in_range(range),
}
}
}

View file

@ -25,7 +25,7 @@ use wrapper::{TLayoutNode, ThreadSafeLayoutNode};
use geom::{Point2D, Rect, Size2D};
use gfx::display_list::OpaqueNode;
use gfx::text::glyph::CharIndex;
use gfx::text::text_run::TextRun;
use gfx::text::text_run::{TextRun, TextRunSlice};
use script_traits::UntrustedNodeAddress;
use serialize::{Encodable, Encoder};
use servo_msg::constellation_msg::{PipelineId, SubpageId};
@ -44,8 +44,8 @@ use string_cache::Atom;
use style::{ComputedValues, TElement, TNode, cascade_anonymous};
use style::computed_values::{LengthOrPercentage, LengthOrPercentageOrAuto};
use style::computed_values::{LengthOrPercentageOrNone};
use style::computed_values::{LPA_Auto, clear, position, text_align, text_decoration};
use style::computed_values::{vertical_align, white_space};
use style::computed_values::{LPA_Auto, clear, overflow_wrap, position, text_align};
use style::computed_values::{text_decoration, vertical_align, white_space};
use sync::{Arc, Mutex};
use url::Url;
@ -395,6 +395,8 @@ impl ScannedTextFragmentInfo {
}
}
/// Describes how to split a fragment. This is used during line breaking as part of the return
/// value of `find_split_info_for_inline_size()`.
#[deriving(Show)]
pub struct SplitInfo {
// TODO(bjz): this should only need to be a single character index, but both values are
@ -412,6 +414,16 @@ impl SplitInfo {
}
}
/// Describes how to split a fragment into two. This contains up to two `SplitInfo`s.
pub struct SplitResult {
/// The part of the fragment that goes on the first line.
pub inline_start: Option<SplitInfo>,
/// The part of the fragment that goes on the second line.
pub inline_end: Option<SplitInfo>,
/// The text run which is being split.
pub text_run: Arc<Box<TextRun>>,
}
/// Data for an unscanned text fragment. Unscanned text fragments are the results of flow
/// construction that have not yet had their inline-size determined.
#[deriving(Clone)]
@ -1097,104 +1109,149 @@ impl Fragment {
}
}
/// Attempts to find the split positions of a text fragment so that its inline-size is
/// no more than `max_inline-size`.
/// Attempts to find the split positions of a text fragment so that its inline-size is no more
/// than `max_inline_size`.
///
/// A return value of `None` indicates that the fragment could not be split.
/// Otherwise the information pertaining to the split is returned. The inline-start
/// and inline-end split information are both optional due to the possibility of
/// them being whitespace.
pub fn find_split_info_for_inline_size(&self,
start: CharIndex,
max_inline_size: Au,
starts_line: bool)
-> Option<(Option<SplitInfo>,
Option<SplitInfo>,
Arc<Box<TextRun>>)> {
match self.specific {
/// A return value of `None` indicates that the fragment could not be split. Otherwise the
/// information pertaining to the split is returned. The inline-start and inline-end split
/// information are both optional due to the possibility of them being whitespace.
pub fn calculate_split_position(&self, max_inline_size: Au, starts_line: bool)
-> Option<SplitResult> {
let text_fragment_info = match self.specific {
GenericFragment | IframeFragment(_) | ImageFragment(_) | TableFragment |
TableCellFragment | TableRowFragment | TableWrapperFragment | InlineBlockFragment(_) |
InlineAbsoluteHypotheticalFragment(_) => None,
InlineAbsoluteHypotheticalFragment(_) => return None,
TableColumnFragment(_) => panic!("Table column fragments do not have inline_size"),
UnscannedTextFragment(_) => {
panic!("Unscanned text fragments should have been scanned by now!")
}
ScannedTextFragment(ref text_fragment_info) => {
let mut pieces_processed_count: uint = 0;
let mut remaining_inline_size: Au = max_inline_size;
let mut inline_start_range = Range::new(text_fragment_info.range.begin() + start,
CharIndex(0));
let mut inline_end_range: Option<Range<CharIndex>> = None;
ScannedTextFragment(ref text_fragment_info) => text_fragment_info,
};
debug!("split_to_inline_size: splitting text fragment \
(strlen={}, range={}, avail_inline_size={})",
text_fragment_info.run.text.len(),
text_fragment_info.range,
max_inline_size);
for (glyphs, offset, slice_range) in text_fragment_info.run.iter_slices_for_range(
&text_fragment_info.range) {
debug!("split_to_inline_size: considering slice (offset={}, range={}, \
remain_inline_size={})",
offset,
slice_range,
remaining_inline_size);
let metrics = text_fragment_info.run.metrics_for_slice(glyphs, &slice_range);
let advance = metrics.advance_width;
let should_continue;
if advance <= remaining_inline_size || glyphs.is_whitespace() {
should_continue = true;
if starts_line && pieces_processed_count == 0 && glyphs.is_whitespace() {
debug!("split_to_inline_size: case=skipping leading trimmable whitespace");
inline_start_range.shift_by(slice_range.length());
} else {
debug!("split_to_inline_size: case=enlarging span");
remaining_inline_size = remaining_inline_size - advance;
inline_start_range.extend_by(slice_range.length());
}
} else {
// The advance is more than the remaining inline-size.
should_continue = false;
let slice_begin = offset + slice_range.begin();
if slice_begin < text_fragment_info.range.end() {
// There are still some things inline-start over at the end of the line. Create
// the inline-end chunk.
let inline_end_range_end = text_fragment_info.range.end() - slice_begin;
inline_end_range = Some(Range::new(slice_begin, inline_end_range_end));
debug!("split_to_inline_size: case=splitting remainder with inline_end range={}",
inline_end_range);
}
}
pieces_processed_count += 1;
if !should_continue {
break
}
}
let inline_start_is_some = inline_start_range.length() > CharIndex(0);
if (pieces_processed_count == 1 || !inline_start_is_some) && !starts_line {
None
} else {
let inline_start = if inline_start_is_some {
Some(SplitInfo::new(inline_start_range, &**text_fragment_info))
} else {
None
};
let inline_end = inline_end_range.map(|inline_end_range| {
SplitInfo::new(inline_end_range, &**text_fragment_info)
});
Some((inline_start, inline_end, text_fragment_info.run.clone()))
}
let mut flags = SplitOptions::empty();
if starts_line {
flags.insert(STARTS_LINE);
if self.style().get_inheritedtext().overflow_wrap == overflow_wrap::break_word {
flags.insert(RETRY_AT_CHARACTER_BOUNDARIES)
}
}
let natural_word_breaking_strategy =
text_fragment_info.run.natural_word_slices_in_range(&text_fragment_info.range);
self.calculate_split_position_using_breaking_strategy(natural_word_breaking_strategy,
max_inline_size,
flags)
}
/// A helper method that uses the breaking strategy described by `slice_iterator` (at present,
/// either natural word breaking or character breaking) to split this fragment.
fn calculate_split_position_using_breaking_strategy<'a,I>(&self,
mut slice_iterator: I,
max_inline_size: Au,
flags: SplitOptions)
-> Option<SplitResult>
where I: Iterator<TextRunSlice<'a>> {
let text_fragment_info = match self.specific {
GenericFragment | IframeFragment(_) | ImageFragment(_) | TableFragment |
TableCellFragment | TableRowFragment | TableWrapperFragment | InlineBlockFragment(_) |
InlineAbsoluteHypotheticalFragment(_) => return None,
TableColumnFragment(_) => panic!("Table column fragments do not have inline_size"),
UnscannedTextFragment(_) => {
panic!("Unscanned text fragments should have been scanned by now!")
}
ScannedTextFragment(ref text_fragment_info) => text_fragment_info,
};
let mut pieces_processed_count: uint = 0;
let mut remaining_inline_size = max_inline_size;
let mut inline_start_range = Range::new(text_fragment_info.range.begin(), CharIndex(0));
let mut inline_end_range = None;
debug!("calculate_split_position: splitting text fragment (strlen={}, range={}, \
max_inline_size={})",
text_fragment_info.run.text.len(),
text_fragment_info.range,
max_inline_size);
for slice in slice_iterator {
debug!("calculate_split_position: considering slice (offset={}, slice range={}, \
remaining_inline_size={})",
slice.offset,
slice.range,
remaining_inline_size);
let metrics = text_fragment_info.run.metrics_for_slice(slice.glyphs, &slice.range);
let advance = metrics.advance_width;
// Have we found the split point?
if advance <= remaining_inline_size || slice.glyphs.is_whitespace() {
// Keep going; we haven't found the split point yet.
if flags.contains(STARTS_LINE) && pieces_processed_count == 0 &&
slice.glyphs.is_whitespace() {
debug!("calculate_split_position: skipping leading trimmable whitespace");
inline_start_range.shift_by(slice.range.length());
} else {
debug!("split_to_inline_size: enlarging span");
remaining_inline_size = remaining_inline_size - advance;
inline_start_range.extend_by(slice.range.length());
}
pieces_processed_count += 1;
continue
}
// The advance is more than the remaining inline-size, so split here.
let slice_begin = slice.text_run_range().begin();
if slice_begin < text_fragment_info.range.end() {
// There still some things left over at the end of the line, so create the
// inline-end chunk.
let mut inline_end = slice.text_run_range();
inline_end.extend_to(text_fragment_info.range.end());
inline_end_range = Some(inline_end);
debug!("calculate_split_position: splitting remainder with inline-end range={}",
inline_end);
}
pieces_processed_count += 1;
break
}
// If we failed to find a suitable split point, we're on the verge of overflowing the line.
let inline_start_is_some = inline_start_range.length() > CharIndex(0);
if pieces_processed_count == 1 || !inline_start_is_some {
// If we've been instructed to retry at character boundaries (probably via
// `overflow-wrap: break-word`), do so.
if flags.contains(RETRY_AT_CHARACTER_BOUNDARIES) {
let character_breaking_strategy =
text_fragment_info.run.character_slices_in_range(&text_fragment_info.range);
let mut flags = flags;
flags.remove(RETRY_AT_CHARACTER_BOUNDARIES);
return self.calculate_split_position_using_breaking_strategy(
character_breaking_strategy,
max_inline_size,
flags)
}
// We aren't at the start of the line, so don't overflow. Let inline layout wrap to the
// next line instead.
if !flags.contains(STARTS_LINE) {
return None
}
}
let inline_start = if inline_start_is_some {
Some(SplitInfo::new(inline_start_range, &**text_fragment_info))
} else {
None
};
let inline_end = inline_end_range.map(|inline_end_range| {
SplitInfo::new(inline_end_range, &**text_fragment_info)
});
Some(SplitResult {
inline_start: inline_start,
inline_end: inline_end,
text_run: text_fragment_info.run.clone(),
})
}
/// Returns true if this fragment is an unscanned text fragment that consists entirely of
@ -1531,6 +1588,18 @@ bitflags! {
}
}
bitflags! {
// Various flags we can use when splitting fragments. See
// `calculate_split_position_using_breaking_strategy()`.
flags SplitOptions: u8 {
#[doc="True if this is the first fragment on the line."]
const STARTS_LINE = 0x01,
#[doc="True if we should attempt to split at character boundaries if this split fails. \
This is used to implement `overflow-wrap: break-word`."]
const RETRY_AT_CHARACTER_BOUNDARIES = 0x02
}
}
/// A top-down fragment bounds iteration handler.
pub trait FragmentBoundsIterator {
/// The operation to perform.
@ -1540,3 +1609,4 @@ pub trait FragmentBoundsIterator {
/// we skip the operation for this fragment, but continue processing siblings.
fn should_process(&mut self, fragment: &Fragment) -> bool;
}

View file

@ -512,18 +512,17 @@ impl LineBreaker {
let available_inline_size = green_zone.inline - self.pending_line.bounds.size.inline -
indentation;
let (inline_start_fragment, inline_end_fragment) =
match fragment.find_split_info_for_inline_size(CharIndex(0),
available_inline_size,
self.pending_line_is_empty()) {
match fragment.calculate_split_position(available_inline_size,
self.pending_line_is_empty()) {
None => {
debug!("LineBreaker: fragment was unsplittable; deferring to next line: {}",
fragment);
self.work_list.push_front(fragment);
return false
}
Some((start_split_info, end_split_info, run)) => {
Some(split_result) => {
let split_fragment = |split: SplitInfo| {
let info = box ScannedTextFragmentInfo::new(run.clone(),
let info = box ScannedTextFragmentInfo::new(split_result.text_run.clone(),
split.range,
Vec::new(),
fragment.border_box.size);
@ -532,8 +531,8 @@ impl LineBreaker {
fragment.border_box.size.block);
fragment.transform(size, info)
};
(start_split_info.map(|x| split_fragment(x)),
end_split_info.map(|x| split_fragment(x)))
(split_result.inline_start.map(|x| split_fragment(x)),
split_result.inline_end.map(|x| split_fragment(x)))
}
};

View file

@ -1136,6 +1136,10 @@ pub mod longhands {
${predefined_type("text-indent", "LengthOrPercentage", "computed::LP_Length(Au(0))")}
// Also known as "word-wrap" (which is more popular because of IE), but this is the preferred
// name per CSS-TEXT 6.2.
${single_keyword("overflow-wrap", "normal break-word")}
${new_style_struct("Text", is_inherited=False)}
<%self:longhand name="text-decoration">
@ -1747,6 +1751,15 @@ pub mod shorthands {
})
</%self:shorthand>
// Per CSS-TEXT 6.2, "for legacy reasons, UAs must treat `word-wrap` as an alternate name for
// the `overflow-wrap` property, as if it were a shorthand of `overflow-wrap`."
<%self:shorthand name="word-wrap" sub_properties="overflow-wrap">
overflow_wrap::parse(input, base_url).map(|specified_value| {
Longhands {
overflow_wrap: Some(specified_value),
}
})
</%self:shorthand>
}

View file

@ -199,3 +199,4 @@ fragment=top != ../html/acid2.html acid2_ref.html
!= border_black_ridge.html border_black_groove.html
== text_indent_a.html text_indent_ref.html
== word_spacing_a.html word_spacing_ref.html
== overflow_wrap_a.html overflow_wrap_ref.html

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<!-- Tests that `overflow-wrap: break-word` breaks words if it needs to, but only when
necessary. -->
<link rel="stylesheet" type="text/css" href="css/ahem.css">
<style>
html, body {
margin: 0;
}
section {
word-wrap: break-word;
width: 300px;
color: purple;
}
</style>
</head>
<body>
<section>X XXXXXX</section>
</body>
</html>

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<!-- Tests that `overflow-wrap: break-word` breaks words if it needs to, but only when
necessary. -->
<style>
section, nav {
background: purple;
position: absolute;
left: 0;
}
section {
width: 100px;
top: 0;
height: 100px;
}
nav {
top: 100px;
width: 300px;
height: 200px;
}
</style>
</head>
<body>
<section></section><nav></nav>
</body>
</html>