-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
inline.rs
2441 lines (2195 loc) · 104 KB
/
inline.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! # Inline Formatting Context Layout
//!
//! Inline layout is divided into three phases:
//!
//! 1. Box Tree Construction
//! 2. Box to Line Layout
//! 3. Line to Fragment Layout
//!
//! The first phase happens during normal box tree constrution, while the second two phases happen
//! during fragment tree construction (sometimes called just "layout").
//!
//! ## Box Tree Construction
//!
//! During box tree construction, DOM elements are transformed into a box tree. This phase collects
//! all of the inline boxes, text, atomic inline elements (boxes with `display: inline-block` or
//! `display: inline-table` as well as things like images and canvas), absolutely positioned blocks,
//! and floated blocks.
//!
//! During the last part of this phase, whitespace is collapsed and text is segmented into
//! [`TextRun`]s based on script, chosen font, and line breaking opportunities. In addition, default
//! fonts are selected for every inline box. Each segment of text is shaped using HarfBuzz and
//! turned into a series of glyphs, which all have a size and a position relative to the origin of
//! the [`TextRun`] (calculated in later phases).
//!
//! The code for this phase is mainly in `construct.rs`, but text handling can also be found in
//! `text_runs.rs.`
//!
//! ## Box to Line Layout
//!
//! During the first phase of fragment tree construction, box tree items are laid out into
//! [`LineItem`]s and fragmented based on line boundaries. This is where line breaking happens. This
//! part of layout fragments boxes and their contents across multiple lines while positioning floats
//! and making sure non-floated contents flow around them. In addition, all atomic elements are laid
//! out, which may descend into their respective trees and create fragments. Finally, absolutely
//! positioned content is collected in order to later hoist it to the containing block for
//! absolutes.
//!
//! Note that during this phase, layout does not know the final block position of content. Only
//! during line to fragment layout, are the final block positions calculated based on the line's
//! final content and its vertical alignment. Instead, positions and line heights are calculated
//! relative to the line's final baseline which will be determined in the final phase.
//!
//! [`LineItem`]s represent a particular set of content on a line. Currently this is represented by
//! a linear series of items that describe the line's hierarchy of inline boxes and content. The
//! item types are:
//!
//! - [`LineItem::TextRun`]
//! - [`LineItem::StartInlineBox`]
//! - [`LineItem::EndInlineBox`]
//! - [`LineItem::Atomic`]
//! - [`LineItem::AbsolutelyPositioned`]
//! - [`LineItem::Float`]
//!
//! The code for this can be found by looking for methods of the form `layout_into_line_item()`.
//!
//! ## Line to Fragment Layout
//!
//! During the second phase of fragment tree construction, the final block position of [`LineItem`]s
//! is calculated and they are converted into [`Fragment`]s. After layout, the [`LineItem`]s are
//! discarded and the new fragments are incorporated into the fragment tree. The final static
//! position of absolutely positioned content is calculated and it is hoisted to its containing
//! block via [`PositioningContext`].
//!
//! The code for this phase, can mainly be found in `line.rs`.
//!
use std::cell::OnceCell;
use std::mem;
use app_units::Au;
use bitflags::bitflags;
use gfx::font::FontMetrics;
use gfx::text::glyph::GlyphStore;
use serde::Serialize;
use servo_arc::Arc;
use style::computed_values::vertical_align::T as VerticalAlign;
use style::computed_values::white_space::T as WhiteSpace;
use style::context::QuirksMode;
use style::logical_geometry::WritingMode;
use style::properties::ComputedValues;
use style::values::computed::Length;
use style::values::generics::box_::VerticalAlignKeyword;
use style::values::generics::text::LineHeight;
use style::values::specified::text::{TextAlignKeyword, TextDecorationLine};
use style::values::specified::{TextAlignLast, TextJustify};
use style::Zero;
use webrender_api::FontInstanceKey;
use super::float::PlacementAmongFloats;
use super::line::{
layout_line_items, AbsolutelyPositionedLineItem, AtomicLineItem, FloatLineItem,
InlineBoxLineItem, LineItem, LineItemLayoutState, LineMetrics, TextRunLineItem,
};
use super::text_run::{add_or_get_font, get_font_for_first_font_for_style, TextRun};
use super::CollapsibleWithParentStartMargin;
use crate::cell::ArcRefCell;
use crate::context::LayoutContext;
use crate::flow::float::{FloatBox, SequentialLayoutState};
use crate::flow::FlowLayout;
use crate::formatting_contexts::{
Baselines, IndependentFormattingContext, NonReplacedFormattingContextContents,
};
use crate::fragment_tree::{
BaseFragmentInfo, BoxFragment, CollapsedBlockMargins, CollapsedMargin, Fragment, FragmentFlags,
PositioningFragment,
};
use crate::geom::{LogicalRect, LogicalVec2};
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
use crate::sizing::ContentSizes;
use crate::style_ext::{ComputedValuesExt, PaddingBorderMargin};
use crate::ContainingBlock;
// From gfxFontConstants.h in Firefox.
static FONT_SUBSCRIPT_OFFSET_RATIO: f32 = 0.20;
static FONT_SUPERSCRIPT_OFFSET_RATIO: f32 = 0.34;
#[derive(Debug, Serialize)]
pub(crate) struct InlineFormattingContext {
pub(super) inline_level_boxes: Vec<ArcRefCell<InlineLevelBox>>,
/// A store of font information for all the shaped segments in this formatting
/// context in order to avoid duplicating this information.
pub font_metrics: Vec<FontKeyAndMetrics>,
pub(super) text_decoration_line: TextDecorationLine,
/// Whether this IFC contains the 1st formatted line of an element:
/// <https://www.w3.org/TR/css-pseudo-4/#first-formatted-line>.
pub(super) has_first_formatted_line: bool,
/// Whether or not this [`InlineFormattingContext`] contains floats.
pub(super) contains_floats: bool,
}
/// A collection of data used to cache [`FontMetrics`] in the [`InlineFormattingContext`]
#[derive(Debug, Serialize)]
pub(crate) struct FontKeyAndMetrics {
pub key: FontInstanceKey,
pub pt_size: Au,
pub metrics: FontMetrics,
}
#[derive(Debug, Serialize)]
pub(crate) enum InlineLevelBox {
InlineBox(InlineBox),
TextRun(TextRun),
OutOfFlowAbsolutelyPositionedBox(ArcRefCell<AbsolutelyPositionedBox>),
OutOfFlowFloatBox(FloatBox),
Atomic(IndependentFormattingContext),
}
#[derive(Debug, Serialize)]
pub(crate) struct InlineBox {
pub base_fragment_info: BaseFragmentInfo,
#[serde(skip_serializing)]
pub style: Arc<ComputedValues>,
pub is_first_fragment: bool,
pub is_last_fragment: bool,
pub children: Vec<ArcRefCell<InlineLevelBox>>,
/// The index of the default font in the [`InlineFormattingContext`]'s font metrics store.
/// This is initialized during IFC shaping.
pub default_font_index: Option<usize>,
}
/// Information about the current line under construction for a particular
/// [`InlineFormattingContextState`]. This tracks position and size information while
/// [`LineItem`]s are collected and is used as input when those [`LineItem`]s are
/// converted into [`Fragment`]s during the final phase of line layout. Note that this
/// does not store the [`LineItem`]s themselves, as they are stored as part of the
/// nesting state in the [`InlineFormattingContextState`].
struct LineUnderConstruction {
/// The position where this line will start once it is laid out. This includes any
/// offset from `text-indent`.
start_position: LogicalVec2<Length>,
/// The current inline position in the line being laid out into [`LineItem`]s in this
/// [`InlineFormattingContext`] independent of the depth in the nesting level.
inline_position: Length,
/// The maximum block size of all boxes that ended and are in progress in this line.
/// This uses [`LineBlockSizes`] instead of a simple value, because the final block size
/// depends on vertical alignment.
max_block_size: LineBlockSizes,
/// Whether any active linebox has added a glyph or atomic element to this line, which
/// indicates that the next run that exceeds the line length can cause a line break.
has_content: bool,
/// Whether or not there are floats that did not fit on the current line. Before
/// the [`LineItem`]s of this line are laid out, these floats will need to be
/// placed directly below this line, but still as children of this line's Fragments.
has_floats_waiting_to_be_placed: bool,
/// A rectangular area (relative to the containing block / inline formatting
/// context boundaries) where we can fit the line box without overlapping floats.
/// Note that when this is not empty, its start corner takes precedence over
/// [`LineUnderConstruction::start_position`].
placement_among_floats: OnceCell<LogicalRect<Length>>,
/// The LineItems for the current line under construction that have already
/// been committed to this line.
line_items: Vec<LineItem>,
}
impl LineUnderConstruction {
fn new(start_position: LogicalVec2<Length>) -> Self {
Self {
inline_position: start_position.inline,
start_position,
max_block_size: LineBlockSizes::zero(),
has_content: false,
has_floats_waiting_to_be_placed: false,
placement_among_floats: OnceCell::new(),
line_items: Vec::new(),
}
}
fn line_block_start_considering_placement_among_floats(&self) -> Au {
match self.placement_among_floats.get() {
Some(placement_among_floats) => placement_among_floats.start_corner.block.into(),
None => self.start_position.block.into(),
}
}
fn replace_placement_among_floats(&mut self, new_placement: LogicalRect<Length>) {
self.placement_among_floats.take();
let _ = self.placement_among_floats.set(new_placement);
}
/// Trim the trailing whitespace in this line and return the width of the whitespace trimmed.
fn trim_trailing_whitespace(&mut self) -> Length {
// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>:
// > 3. A sequence of collapsible spaces at the end of a line is removed,
// > as well as any trailing U+1680 OGHAM SPACE MARK whose white-space
// > property is normal, nowrap, or pre-line.
let mut whitespace_trimmed = Length::zero();
for item in self.line_items.iter_mut().rev() {
if !item.trim_whitespace_at_end(&mut whitespace_trimmed) {
break;
}
}
whitespace_trimmed
}
/// Count the number of justification opportunities in this line.
fn count_justification_opportunities(&self) -> usize {
self.line_items
.iter()
.filter_map(|item| match item {
LineItem::TextRun(text_run) => Some(
text_run
.text
.iter()
.map(|glyph_store| glyph_store.total_word_separators())
.sum::<usize>(),
),
_ => None,
})
.sum()
}
}
/// A block size relative to a line's final baseline. This is to track the size
/// contribution of a particular element of a line above and below the baseline.
/// These sizes can be combined with other baseline relative sizes before the
/// final baseline position is known. The values here are relative to the
/// overall line's baseline and *not* the nested baseline of an inline box.
#[derive(Clone, Debug)]
struct BaselineRelativeSize {
/// The ascent above the baseline, where a positive value means a larger
/// ascent. Thus, the top of this size contribution is `baseline_offset -
/// ascent`.
ascent: Au,
/// The descent below the baseline, where a positive value means a larger
/// descent. Thus, the bottom of this size contribution is `baseline_offset +
/// descent`.
descent: Au,
}
impl BaselineRelativeSize {
fn zero() -> Self {
Self {
ascent: Au::zero(),
descent: Au::zero(),
}
}
fn max(&self, other: &Self) -> Self {
BaselineRelativeSize {
ascent: self.ascent.max(other.ascent),
descent: self.descent.max(other.descent),
}
}
/// Given an offset from the line's root baseline, adjust this [`BaselineRelativeSize`]
/// by that offset. This is used to adjust a [`BaselineRelativeSize`] for different kinds
/// of baseline-relative `vertical-align`. This will "move" measured size of a particular
/// inline box's block size. For example, in the following HTML:
///
/// ```html
/// <div>
/// <span style="vertical-align: 5px">child content</span>
/// </div>
/// ````
///
/// If this [`BaselineRelativeSize`] is for the `<span>` then the adjustment
/// passed here would be equivalent to -5px.
fn adjust_for_nested_baseline_offset(&mut self, baseline_offset: Au) {
self.ascent -= baseline_offset;
self.descent += baseline_offset;
}
}
#[derive(Clone, Debug)]
struct LineBlockSizes {
line_height: Length,
baseline_relative_size_for_line_height: Option<BaselineRelativeSize>,
size_for_baseline_positioning: BaselineRelativeSize,
}
impl LineBlockSizes {
fn zero() -> Self {
LineBlockSizes {
line_height: Length::zero(),
baseline_relative_size_for_line_height: None,
size_for_baseline_positioning: BaselineRelativeSize::zero(),
}
}
fn resolve(&self) -> Length {
let height_from_ascent_and_descent = self
.baseline_relative_size_for_line_height
.as_ref()
.map(|size| (size.ascent + size.descent).abs())
.unwrap_or_else(Au::zero);
self.line_height.max(height_from_ascent_and_descent.into())
}
fn max(&self, other: &LineBlockSizes) -> LineBlockSizes {
let baseline_relative_size = match (
self.baseline_relative_size_for_line_height.as_ref(),
other.baseline_relative_size_for_line_height.as_ref(),
) {
(Some(our_size), Some(other_size)) => Some(our_size.max(other_size)),
(our_size, other_size) => our_size.or(other_size).cloned(),
};
Self {
line_height: self.line_height.max(other.line_height),
baseline_relative_size_for_line_height: baseline_relative_size,
size_for_baseline_positioning: self
.size_for_baseline_positioning
.max(&other.size_for_baseline_positioning),
}
}
fn max_assign(&mut self, other: &LineBlockSizes) {
*self = self.max(other);
}
fn adjust_for_baseline_offset(&mut self, baseline_offset: Au) {
if let Some(size) = self.baseline_relative_size_for_line_height.as_mut() {
size.adjust_for_nested_baseline_offset(baseline_offset)
}
self.size_for_baseline_positioning
.adjust_for_nested_baseline_offset(baseline_offset);
}
/// From <https://drafts.csswg.org/css2/visudet.html#line-height>:
/// > The inline-level boxes are aligned vertically according to their 'vertical-align'
/// > property. In case they are aligned 'top' or 'bottom', they must be aligned so as
/// > to minimize the line box height. If such boxes are tall enough, there are multiple
/// > solutions and CSS 2 does not define the position of the line box's baseline (i.e.,
/// > the position of the strut, see below).
fn find_baseline_offset(&self) -> Au {
match self.baseline_relative_size_for_line_height.as_ref() {
Some(size) => size.ascent,
None => {
// This is the case mentinoned above where there are multiple solutions.
// This code is putting the baseline roughly in the middle of the line.
let leading = Au::from(self.resolve()) -
(self.size_for_baseline_positioning.ascent +
self.size_for_baseline_positioning.descent);
leading.scale_by(0.5) + self.size_for_baseline_positioning.ascent
},
}
}
}
/// The current unbreakable segment under construction for an inline formatting context.
/// Items accumulate here until we reach a soft line break opportunity during processing
/// of inline content or we reach the end of the formatting context.
struct UnbreakableSegmentUnderConstruction {
/// The size of this unbreakable segment in both dimension.
inline_size: Length,
/// The maximum block size that this segment has. This uses [`LineBlockSizes`] instead of a
/// simple value, because the final block size depends on vertical alignment.
max_block_size: LineBlockSizes,
/// The LineItems for the segment under construction
line_items: Vec<LineItem>,
/// The depth in the inline box hierarchy at the start of this segment. This is used
/// to prefix this segment when it is pushed to a new line.
inline_box_hierarchy_depth: Option<usize>,
/// Whether any active linebox has added a glyph or atomic element to this line
/// segment, which indicates that the next run that exceeds the line length can cause
/// a line break.
has_content: bool,
/// The inline size of any trailing whitespace in this segment.
trailing_whitespace_size: Length,
}
impl UnbreakableSegmentUnderConstruction {
fn new() -> Self {
Self {
inline_size: Length::zero(),
max_block_size: LineBlockSizes {
line_height: Length::zero(),
baseline_relative_size_for_line_height: None,
size_for_baseline_positioning: BaselineRelativeSize::zero(),
},
line_items: Vec::new(),
inline_box_hierarchy_depth: None,
has_content: false,
trailing_whitespace_size: Length::zero(),
}
}
/// Reset this segment after its contents have been committed to a line.
fn reset(&mut self) {
assert!(self.line_items.is_empty()); // Preserve allocated memory.
self.inline_size = Length::zero();
self.max_block_size = LineBlockSizes::zero();
self.inline_box_hierarchy_depth = None;
self.has_content = false;
self.trailing_whitespace_size = Length::zero();
}
/// Push a single line item to this segment. In addition, record the inline box
/// hierarchy depth if this is the first segment. The hierarchy depth is used to
/// duplicate the necessary `StartInlineBox` tokens if this segment is ultimately
/// placed on a new empty line.
fn push_line_item(&mut self, line_item: LineItem, inline_box_hierarchy_depth: usize) {
if self.line_items.is_empty() {
self.inline_box_hierarchy_depth = Some(inline_box_hierarchy_depth);
}
self.line_items.push(line_item);
}
/// Trim whitespace from the beginning of this UnbreakbleSegmentUnderConstruction.
///
/// From <https://www.w3.org/TR/css-text-3/#white-space-phase-2>:
///
/// > Then, the entire block is rendered. Inlines are laid out, taking bidi
/// > reordering into account, and wrapping as specified by the text-wrap
/// > property. As each line is laid out,
/// > 1. A sequence of collapsible spaces at the beginning of a line is removed.
///
/// This prevents whitespace from being added to the beginning of a line.
fn trim_leading_whitespace(&mut self) {
let mut whitespace_trimmed = Length::zero();
for item in self.line_items.iter_mut() {
if !item.trim_whitespace_at_start(&mut whitespace_trimmed) {
break;
}
}
self.inline_size -= whitespace_trimmed;
}
/// Prepare this segment for placement on a new and empty line. This happens when the
/// segment is too large to fit on the current line and needs to be placed on a new
/// one.
fn prepare_for_placement_on_empty_line(
&mut self,
line: &LineUnderConstruction,
current_hierarchy_depth: usize,
) {
self.trim_leading_whitespace();
// The segment may start in the middle of an already processed inline box. In that
// case we need to duplicate the `StartInlineBox` tokens as a prefix of the new
// lines. For instance if the following segment is going to be placed on a new line:
//
// line = [StartInlineBox "every"]
// segment = ["good" EndInlineBox "boy"]
//
// Then the segment must be prefixed with `StartInlineBox` before it is committed
// to the empty line.
let mut hierarchy_depth = self
.inline_box_hierarchy_depth
.unwrap_or(current_hierarchy_depth);
if hierarchy_depth == 0 {
return;
}
let mut hierarchy = Vec::new();
let mut skip_depth = 0;
for item in line.line_items.iter().rev() {
match item {
// We need to skip over any inline boxes that are not in our hierarchy. If
// any inline box ends, we skip until it starts.
LineItem::StartInlineBox(_) if skip_depth > 0 => skip_depth -= 1,
LineItem::EndInlineBox => skip_depth += 1,
// Otherwise copy the inline box to the hierarchy we are collecting.
LineItem::StartInlineBox(inline_box) => {
let mut cloned_inline_box = inline_box.clone();
cloned_inline_box.is_first_fragment = false;
hierarchy.push(LineItem::StartInlineBox(cloned_inline_box));
hierarchy_depth -= 1;
if hierarchy_depth == 0 {
break;
}
},
_ => {},
}
}
let segment_items = mem::take(&mut self.line_items);
self.line_items = hierarchy.into_iter().rev().chain(segment_items).collect();
}
}
struct InlineContainerState {
/// The style of this inline container.
style: Arc<ComputedValues>,
/// Whether or not we have processed any content (an atomic element or text) for
/// this inline box on the current line OR any previous line.
has_content: bool,
/// Indicates whether this nesting level have text decorations in effect.
/// From <https://drafts.csswg.org/css-text-decor/#line-decoration>
// "When specified on or propagated to a block container that establishes
// an IFC..."
text_decoration_line: TextDecorationLine,
/// The block size contribution of this container's default font ie the size of the
/// "strut." Whether this is integrated into the [`Self::nested_strut_block_sizes`]
/// depends on the line-height quirk described in
/// <https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk>.
strut_block_sizes: LineBlockSizes,
/// The strut block size of this inline container maxed with the strut block
/// sizes of all inline container ancestors. In quirks mode, this will be
/// zero, until we know that an element has inline content.
nested_strut_block_sizes: LineBlockSizes,
/// The baseline offset of this container from the baseline of the line. The is the
/// cumulative offset of this container and all of its parents. In contrast to the
/// `vertical-align` property a positive value indicates an offset "below" the
/// baseline while a negative value indicates one "above" it (when the block direction
/// is vertical).
baseline_offset: Au,
/// The font metrics of the non-fallback font for this container.
font_metrics: FontMetrics,
}
struct InlineBoxContainerState {
/// The container state common to both [`InlineBox`] and the root of the
/// [`InlineFormattingContext`].
base: InlineContainerState,
/// The [`BaseFragmentInfo`] of the [`InlineBox`] that this state tracks.
base_fragment_info: BaseFragmentInfo,
/// The [`PaddingBorderMargin`] of the [`InlineBox`] that this state tracks.
pbm: PaddingBorderMargin,
/// Whether this is the last fragment of this InlineBox. This may not be the case if
/// the InlineBox is split due to an block-in-inline-split and this is not the last of
/// that split.
is_last_fragment: bool,
}
pub(super) struct InlineFormattingContextState<'a, 'b> {
positioning_context: &'a mut PositioningContext,
containing_block: &'b ContainingBlock<'b>,
sequential_layout_state: Option<&'a mut SequentialLayoutState>,
layout_context: &'b LayoutContext<'b>,
/// The list of [`FontMetrics`] used by the [`InlineFormattingContext`] that
/// we are laying out.
fonts: &'a Vec<FontKeyAndMetrics>,
/// The [`InlineContainerState`] for the container formed by the root of the
/// [`InlineFormattingContext`]. This is effectively the "root inline box" described
/// by <https://drafts.csswg.org/css-inline/#model>:
///
/// > The block container also generates a root inline box, which is an anonymous
/// > inline box that holds all of its inline-level contents. (Thus, all text in an
/// > inline formatting context is directly contained by an inline box, whether the root
/// > inline box or one of its descendants.) The root inline box inherits from its
/// > parent block container, but is otherwise unstyleable.
root_nesting_level: InlineContainerState,
/// A stack of [`InlineBoxContainerState`] that is used to produce [`LineItem`]s either when we
/// reach the end of an inline box or when we reach the end of a line. Only at the end
/// of the inline box is the state popped from the stack.
inline_box_state_stack: Vec<InlineBoxContainerState>,
/// A vector of fragment that are laid out. This includes one [`Fragment::Positioning`]
/// per line that is currently laid out plus fragments for all floats, which
/// are currently laid out at the top-level of each [`InlineFormattingContext`].
fragments: Vec<Fragment>,
/// Information about the line currently being laid out into [`LineItem`]s.
current_line: LineUnderConstruction,
/// Information about the unbreakable line segment currently being laid out into [`LineItem`]s.
current_line_segment: UnbreakableSegmentUnderConstruction,
/// After a forced line break (for instance from a `<br>` element) we wait to actually
/// break the line until seeing more content. This allows ongoing inline boxes to finish,
/// since in the case where they have no more content they should not be on the next
/// line.
///
/// For instance:
///
/// ``` html
/// <span style="border-right: 30px solid blue;">
/// first line<br>
/// </span>
/// second line
/// ```
///
/// In this case, the `<span>` should not extend to the second line. If we linebreak
/// as soon as we encounter the `<br>` the `<span>`'s ending inline borders would be
/// placed on the second line, because we add those borders in
/// [`InlineFormattingContextState::finish_inline_box()`].
linebreak_before_new_content: bool,
/// Whether or not a soft wrap opportunity is queued. Soft wrap opportunities are
/// queued after replaced content and they are processed when the next text content
/// is encountered.
pub have_deferred_soft_wrap_opportunity: bool,
/// Whether or not a soft wrap opportunity should be prevented before the next atomic
/// element encountered in the inline formatting context. See
/// `char_prevents_soft_wrap_opportunity_when_before_or_after_atomic` for more
/// details.
pub prevent_soft_wrap_opportunity_before_next_atomic: bool,
/// Whether or not this InlineFormattingContext has processed any in flow content at all.
had_inflow_content: bool,
/// The currently white-space setting of this line. This is stored on the
/// [`InlineFormattingContextState`] because when a soft wrap opportunity is defined
/// by the boundary between two characters, the white-space property of their nearest
/// common ancestor is used.
white_space: WhiteSpace,
/// The offset of the first and last baselines in the inline formatting context that we
/// are laying out. This is used to propagate baselines to the ancestors of
/// `display: inline-block` elements and table content.
baselines: Baselines,
}
impl<'a, 'b> InlineFormattingContextState<'a, 'b> {
fn current_inline_container_state(&self) -> &InlineContainerState {
match self.inline_box_state_stack.last() {
Some(inline_box_state) => &inline_box_state.base,
None => &self.root_nesting_level,
}
}
fn current_inline_container_state_mut(&mut self) -> &mut InlineContainerState {
match self.inline_box_state_stack.last_mut() {
Some(inline_box_state) => &mut inline_box_state.base,
None => &mut self.root_nesting_level,
}
}
fn current_line_max_block_size_including_nested_containers(&self) -> LineBlockSizes {
self.current_inline_container_state()
.nested_strut_block_sizes
.max(&self.current_line.max_block_size)
}
fn propagate_current_nesting_level_white_space_style(&mut self) {
let style = match self.inline_box_state_stack.last() {
Some(inline_box_state) => &inline_box_state.base.style,
None => self.containing_block.style,
};
self.white_space = style.get_inherited_text().white_space;
}
fn processing_br_element(&self) -> bool {
self.inline_box_state_stack
.last()
.map(|state| {
state
.base_fragment_info
.flags
.contains(FragmentFlags::IS_BR_ELEMENT)
})
.unwrap_or(false)
}
/// Start laying out a particular [`InlineBox`] into line items. This will push
/// a new [`InlineBoxContainerState`] onto [`Self::inline_box_state_stack`].
fn start_inline_box(&mut self, inline_box: &InlineBox) {
let mut inline_box_state = InlineBoxContainerState::new(
inline_box,
self.containing_block,
self.layout_context,
self.current_inline_container_state(),
inline_box.is_last_fragment,
inline_box
.default_font_index
.map(|index| &self.fonts[index].metrics),
);
if inline_box.is_first_fragment {
self.current_line.inline_position += Length::from(
inline_box_state.pbm.padding.inline_start +
inline_box_state.pbm.border.inline_start,
) + inline_box_state
.pbm
.margin
.inline_start
.auto_is(Au::zero)
.into()
}
let line_item = inline_box_state
.layout_into_line_item(inline_box.is_first_fragment, inline_box.is_last_fragment);
self.push_line_item_to_unbreakable_segment(LineItem::StartInlineBox(line_item));
self.inline_box_state_stack.push(inline_box_state);
}
/// Finish laying out a particular [`InlineBox`] into line items. This will add the
/// final [`InlineBoxLineItem`] to the state and pop its state off of
/// [`Self::inline_box_state_stack`].
fn finish_inline_box(&mut self) {
let inline_box_state = match self.inline_box_state_stack.pop() {
Some(inline_box_state) => inline_box_state,
None => return, // We are at the root.
};
self.push_line_item_to_unbreakable_segment(LineItem::EndInlineBox);
self.current_line_segment
.max_block_size
.max_assign(&inline_box_state.base.nested_strut_block_sizes);
// If the inline box that we just finished had any content at all, we want to propagate
// the `white-space` property of its parent to future inline children. This is because
// when a soft wrap opportunity is defined by the boundary between two elements, the
// `white-space` used is that of their nearest common ancestor.
if inline_box_state.base.has_content {
self.propagate_current_nesting_level_white_space_style();
}
if inline_box_state.is_last_fragment {
let pbm_end = Length::from(
inline_box_state.pbm.padding.inline_end + inline_box_state.pbm.border.inline_end,
) + inline_box_state
.pbm
.margin
.inline_end
.auto_is(Au::zero)
.into();
self.current_line_segment.inline_size += pbm_end;
}
}
fn finish_last_line(&mut self) {
// We are at the end of the IFC, and we need to do a few things to make sure that
// the current segment is committed and that the final line is finished.
//
// A soft wrap opportunity makes it so the current segment is placed on a new line
// if it doesn't fit on the current line under construction.
self.process_soft_wrap_opportunity();
// `process_soft_line_wrap_opportunity` does not commit the segment to a line if
// there is no line wrapping, so this forces the segment into the current line.
self.commit_current_segment_to_line();
// Finally we finish the line itself and convert all of the LineItems into
// fragments.
self.finish_current_line_and_reset(true /* last_line_or_forced_line_break */);
}
/// Finish layout of all inline boxes for the current line. This will gather all
/// [`LineItem`]s and turn them into [`Fragment`]s, then reset the
/// [`InlineFormattingContextState`] preparing it for laying out a new line.
fn finish_current_line_and_reset(&mut self, last_line_or_forced_line_break: bool) {
let whitespace_trimmed = self.current_line.trim_trailing_whitespace();
let (inline_start_position, justification_adjustment) = self
.calculate_current_line_inline_start_and_justification_adjustment(
whitespace_trimmed,
last_line_or_forced_line_break,
);
let block_start_position = self
.current_line
.line_block_start_considering_placement_among_floats();
let had_inline_advance =
self.current_line.inline_position != self.current_line.start_position.inline;
let effective_block_advance = if self.current_line.has_content ||
had_inline_advance ||
self.linebreak_before_new_content
{
self.current_line_max_block_size_including_nested_containers()
} else {
LineBlockSizes::zero()
};
let block_end_position = block_start_position + effective_block_advance.resolve().into();
if let Some(sequential_layout_state) = self.sequential_layout_state.as_mut() {
// This amount includes both the block size of the line and any extra space
// added to move the line down in order to avoid overlapping floats.
let increment = block_end_position - self.current_line.start_position.block.into();
sequential_layout_state.advance_block_position(increment);
}
let mut line_items = std::mem::take(&mut self.current_line.line_items);
if self.current_line.has_floats_waiting_to_be_placed {
place_pending_floats(self, &mut line_items);
}
// Set up the new line now that we no longer need the old one.
self.current_line = LineUnderConstruction::new(LogicalVec2 {
inline: Length::zero(),
block: block_end_position.into(),
});
let baseline_offset = effective_block_advance.find_baseline_offset();
let mut state = LineItemLayoutState {
inline_position: inline_start_position,
parent_offset: LogicalVec2::zero(),
baseline_offset,
ifc_containing_block: self.containing_block,
positioning_context: self.positioning_context,
justification_adjustment,
line_metrics: &LineMetrics {
block_offset: block_start_position.into(),
block_size: effective_block_advance.resolve(),
baseline_block_offset: baseline_offset,
},
};
let positioning_context_length = state.positioning_context.len();
let mut saw_end = false;
let fragments = layout_line_items(
&mut line_items.into_iter(),
self.layout_context,
&mut state,
&mut saw_end,
);
let line_had_content =
!fragments.is_empty() || state.positioning_context.len() != positioning_context_length;
// If the line doesn't have any fragments, we don't need to add a containing fragment for it.
if !line_had_content {
return;
}
let baseline = baseline_offset + block_start_position;
self.baselines.first.get_or_insert(baseline);
self.baselines.last = Some(baseline);
let line_rect = LogicalRect {
// The inline part of this start offset was taken into account when determining
// the inline start of the line in `calculate_inline_start_for_current_line` so
// we do not need to include it in the `start_corner` of the line's main Fragment.
start_corner: LogicalVec2 {
inline: Length::zero(),
block: block_start_position.into(),
},
size: LogicalVec2 {
inline: self.containing_block.inline_size.into(),
block: effective_block_advance.resolve(),
},
};
state
.positioning_context
.adjust_static_position_of_hoisted_fragments_with_offset(
&line_rect.start_corner,
positioning_context_length,
);
self.fragments
.push(Fragment::Positioning(PositioningFragment::new_anonymous(
line_rect,
fragments,
self.containing_block.style.writing_mode,
)));
}
/// Given the amount of whitespace trimmed from the line and taking into consideration
/// the `text-align` property, calculate where the line under construction starts in
/// the inline axis as well as the adjustment needed for every justification opportunity
/// to account for `text-align: justify`.
fn calculate_current_line_inline_start_and_justification_adjustment(
&self,
whitespace_trimmed: Length,
last_line_or_forced_line_break: bool,
) -> (Length, Length) {
enum TextAlign {
Start,
Center,
End,
}
let style = self.containing_block.style;
let mut text_align_keyword = style.clone_text_align();
if last_line_or_forced_line_break {
text_align_keyword = match style.clone_text_align_last() {
TextAlignLast::Auto if text_align_keyword == TextAlignKeyword::Justify => {
TextAlignKeyword::Start
},
TextAlignLast::Auto => text_align_keyword,
TextAlignLast::Start => TextAlignKeyword::Start,
TextAlignLast::End => TextAlignKeyword::End,
TextAlignLast::Left => TextAlignKeyword::Left,
TextAlignLast::Right => TextAlignKeyword::Right,
TextAlignLast::Center => TextAlignKeyword::Center,
TextAlignLast::Justify => TextAlignKeyword::Justify,
};
}
let text_align = match text_align_keyword {
TextAlignKeyword::Start => TextAlign::Start,
TextAlignKeyword::Center | TextAlignKeyword::ServoCenter => TextAlign::Center,
TextAlignKeyword::End => TextAlign::End,
TextAlignKeyword::Left | TextAlignKeyword::ServoLeft => {
if style.writing_mode.line_left_is_inline_start() {
TextAlign::Start
} else {
TextAlign::End
}
},
TextAlignKeyword::Right | TextAlignKeyword::ServoRight => {
if style.writing_mode.line_left_is_inline_start() {
TextAlign::End
} else {
TextAlign::Start
}
},
TextAlignKeyword::Justify => TextAlign::Start,
};
let (line_start, available_space) = match self.current_line.placement_among_floats.get() {
Some(placement_among_floats) => (
placement_among_floats.start_corner.inline,
placement_among_floats.size.inline,
),
None => (Length::zero(), self.containing_block.inline_size.into()),
};
// Properly handling text-indent requires that we do not align the text
// into the text-indent.
// See <https://drafts.csswg.org/css-text/#text-indent-property>
// "This property specifies the indentation applied to lines of inline content in
// a block. The indent is treated as a margin applied to the start edge of the
// line box."
let text_indent = self.current_line.start_position.inline;
let line_length = self.current_line.inline_position - whitespace_trimmed - text_indent;
let adjusted_line_start = line_start +
match text_align {
TextAlign::Start => text_indent,
TextAlign::End => (available_space - line_length).max(text_indent),
TextAlign::Center => {
((available_space - line_length + text_indent) / 2.).max(text_indent)
},
};
// Calculate the justification adjustment. This is simply the remaining space on the line,
// dividided by the number of justficiation opportunities that we recorded when building
// the line.
let text_justify = self.containing_block.style.clone_text_justify();
let justification_adjustment = match (text_align_keyword, text_justify) {
// `text-justify: none` should disable text justification.
// TODO: Handle more `text-justify` values.
(TextAlignKeyword::Justify, TextJustify::None) => Length::zero(),
(TextAlignKeyword::Justify, _) => {
match self.current_line.count_justification_opportunities() {
0 => Length::zero(),
num_justification_opportunities => {
(available_space - line_length) / (num_justification_opportunities as f32)
},
}
},
_ => Length::zero(),
};