Implement inline box continuation geometry
This commit is contained in:
1 parent
cb6fa31d81
commit
642b6b57b5
3 files changed
+809
-71
No files matched your search
@@ -12325,6 +12325,75 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_horizontal_edges_advance_adjacent_content_but_margins_stay_outside_rect() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { font:16px/20px monospace; white-space:nowrap }
|
||||||
|
#token {
|
||||||
|
margin-left:3px; padding:0 4px;
|
||||||
|
border-left:2px solid; border-right:2px solid;
|
||||||
|
margin-right:5px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p id="decorated"><span id="token">aa</span><span id="after">bb</span></p>
|
||||||
|
<p id="plain-host"><span id="plain">aa</span><span id="plain-after">bb</span></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (400.0, 100.0));
|
||||||
|
let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] };
|
||||||
|
let local_x = |id: &str, host: &str| rect(id).x - rect(host).x;
|
||||||
|
|
||||||
|
assert!((rect("token").width - rect("plain").width - 12.0).abs() < 0.01);
|
||||||
|
assert!((local_x("token", "decorated") - local_x("plain", "plain-host") - 3.0).abs() < 0.01);
|
||||||
|
assert!((local_x("after", "decorated") - local_x("plain-after", "plain-host") - 20.0).abs() < 0.01);
|
||||||
|
assert!((rect("after").x - (rect("token").x + rect("token").width) - 5.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inline_edges_participate_in_wrapping_and_slice_only_outer_continuation_sides() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:70px; font:16px/20px monospace }
|
||||||
|
#token { padding:0 10px; border-left:2px solid; border-right:2px solid }
|
||||||
|
</style>
|
||||||
|
<p><span id="token">aaaa aaaa aaaa</span></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (200.0, 100.0));
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let fragments = &laid.inline_fragments[&token];
|
||||||
|
|
||||||
|
assert_eq!(fragments.len(), 3, "{fragments:?}");
|
||||||
|
assert!((fragments[0].width - fragments[1].width - 12.0).abs() < 0.01, "{fragments:?}");
|
||||||
|
assert!(fragments[2].width > fragments[1].width, "last continuation owns its end side: {fragments:?}");
|
||||||
|
assert!(fragments.iter().all(|fragment| fragment.width <= 70.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_decorated_inline_has_a_box_and_advances_following_text() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { font:16px/20px monospace; white-space:nowrap }
|
||||||
|
#empty {
|
||||||
|
margin:0 3px; padding:2px 5px; border:1px solid;
|
||||||
|
border-left-width:2px; border-right-width:2px
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p id="decorated">x<span id="empty"></span><span id="after">y</span></p>
|
||||||
|
<p id="plain-host">x<span id="plain-after">y</span></p>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (300.0, 100.0));
|
||||||
|
let rect = |id: &str| -> Rect { laid.rects[&tree.get_element_by_id(id).unwrap()] };
|
||||||
|
|
||||||
|
assert!((rect("empty").width - 14.0).abs() < 0.01, "{:?}", rect("empty"));
|
||||||
|
assert!((rect("empty").height - laid.text_engine.inline_font_box_height(&laid.styles[&tree.get_element_by_id("empty").unwrap()]) - 6.0).abs() < 0.01);
|
||||||
|
let decorated_after = rect("after").x - rect("decorated").x;
|
||||||
|
let plain_after = rect("plain-after").x - rect("plain-host").x;
|
||||||
|
assert!((decorated_after - plain_after - 20.0).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inline_provenance_maps_repeated_multibyte_text_across_hard_breaks() {
|
fn inline_provenance_maps_repeated_multibyte_text_across_hard_breaks() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -433,6 +433,8 @@ pub struct InlineItem {
|
|||||||
/// with no nested inline owner, keeping that path allocation-free.
|
/// with no nested inline owner, keeping that path allocation-free.
|
||||||
owner_text: Option<String>,
|
owner_text: Option<String>,
|
||||||
owner_ranges: Vec<OwnerTextRange>,
|
owner_ranges: Vec<OwnerTextRange>,
|
||||||
|
owner_boxes: Vec<InlineOwnerBox>,
|
||||||
|
boundary_events: Vec<InlineBoundaryEvent>,
|
||||||
/// Nonzero used relative-position offsets, projected onto text ranges.
|
/// Nonzero used relative-position offsets, projected onto text ranges.
|
||||||
/// Empty for ordinary IFCs and for nested inlines that remain at their
|
/// Empty for ordinary IFCs and for nested inlines that remain at their
|
||||||
/// normal-flow position, so paint pays no provenance cost on that path.
|
/// normal-flow position, so paint pays no provenance cost on that path.
|
||||||
@@ -446,6 +448,51 @@ struct OwnerTextRange {
|
|||||||
end: usize,
|
end: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
struct InlineEdge {
|
||||||
|
margin: f32,
|
||||||
|
border: f32,
|
||||||
|
padding: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InlineEdge {
|
||||||
|
fn advance(self) -> f32 {
|
||||||
|
self.margin + self.border + self.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
fn border_padding(self) -> f32 {
|
||||||
|
self.border + self.padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct InlineOwnerBox {
|
||||||
|
owner: NodeId,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
start_edge: InlineEdge,
|
||||||
|
end_edge: InlineEdge,
|
||||||
|
start_event: usize,
|
||||||
|
end_event: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct InlineBoundaryEvent {
|
||||||
|
owner: NodeId,
|
||||||
|
position: usize,
|
||||||
|
is_start: bool,
|
||||||
|
edge: InlineEdge,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct ActiveInlineOwner {
|
||||||
|
owner: NodeId,
|
||||||
|
start: usize,
|
||||||
|
start_edge: InlineEdge,
|
||||||
|
end_edge: InlineEdge,
|
||||||
|
start_event: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct RelativeOwnerTextRange {
|
struct RelativeOwnerTextRange {
|
||||||
start: usize,
|
start: usize,
|
||||||
@@ -1001,6 +1048,8 @@ impl TextEngine {
|
|||||||
spans,
|
spans,
|
||||||
collector.clip_fills,
|
collector.clip_fills,
|
||||||
collector.owner_ranges,
|
collector.owner_ranges,
|
||||||
|
collector.owner_boxes,
|
||||||
|
collector.boundary_events,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,6 +1104,8 @@ impl TextEngine {
|
|||||||
spans,
|
spans,
|
||||||
collector.clip_fills,
|
collector.clip_fills,
|
||||||
collector.owner_ranges,
|
collector.owner_ranges,
|
||||||
|
collector.owner_boxes,
|
||||||
|
collector.boundary_events,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,6 +1164,8 @@ impl TextEngine {
|
|||||||
spans,
|
spans,
|
||||||
collector.clip_fills,
|
collector.clip_fills,
|
||||||
collector.owner_ranges,
|
collector.owner_ranges,
|
||||||
|
collector.owner_boxes,
|
||||||
|
collector.boundary_events,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1126,6 +1179,8 @@ impl TextEngine {
|
|||||||
mut spans: Vec<(String, SpanAttrs)>,
|
mut spans: Vec<(String, SpanAttrs)>,
|
||||||
clip_fills: Vec<ClipTextFill>,
|
clip_fills: Vec<ClipTextFill>,
|
||||||
mut owner_ranges: Vec<OwnerTextRange>,
|
mut owner_ranges: Vec<OwnerTextRange>,
|
||||||
|
mut owner_boxes: Vec<InlineOwnerBox>,
|
||||||
|
mut boundary_events: Vec<InlineBoundaryEvent>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
let white_space = base.white_space.unwrap_or_default();
|
let white_space = base.white_space.unwrap_or_default();
|
||||||
let layout_wrap = if spans.iter().any(|(_, attrs)| attrs.has_layout_emergency_breaks()) {
|
let layout_wrap = if spans.iter().any(|(_, attrs)| attrs.has_layout_emergency_breaks()) {
|
||||||
@@ -1154,7 +1209,7 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if spans.iter().all(|(text, _)| {
|
if owner_boxes.is_empty() && spans.iter().all(|(text, _)| {
|
||||||
text.is_empty()
|
text.is_empty()
|
||||||
|| (matches!(
|
|| (matches!(
|
||||||
white_space,
|
white_space,
|
||||||
@@ -1172,7 +1227,14 @@ impl TextEngine {
|
|||||||
range.end = range.end.min(text_len);
|
range.end = range.end.min(text_len);
|
||||||
range.start < range.end
|
range.start < range.end
|
||||||
});
|
});
|
||||||
let owner_text = (!owner_ranges.is_empty()).then(|| {
|
for owner in &mut owner_boxes {
|
||||||
|
owner.start = owner.start.min(text_len);
|
||||||
|
owner.end = owner.end.min(text_len);
|
||||||
|
}
|
||||||
|
for event in &mut boundary_events {
|
||||||
|
event.position = event.position.min(text_len);
|
||||||
|
}
|
||||||
|
let owner_text = (!owner_boxes.is_empty()).then(|| {
|
||||||
let mut text = String::with_capacity(text_len);
|
let mut text = String::with_capacity(text_len);
|
||||||
for (span, _) in &spans {
|
for (span, _) in &spans {
|
||||||
text.push_str(span);
|
text.push_str(span);
|
||||||
@@ -1198,6 +1260,11 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
let forced_lines =
|
let forced_lines =
|
||||||
forced_breaks + usize::from(forced_breaks > 0 && visible_after_last_break);
|
forced_breaks + usize::from(forced_breaks > 0 && visible_after_last_break);
|
||||||
|
let forced_lines = if owner_boxes.is_empty() {
|
||||||
|
forced_lines
|
||||||
|
} else {
|
||||||
|
forced_lines.max(1)
|
||||||
|
};
|
||||||
let forced_min_height = forced_lines as f32 * line_h.max(1.0);
|
let forced_min_height = forced_lines as f32 * line_h.max(1.0);
|
||||||
// cosmic-text asserts (an uncatchable process abort) if font size OR
|
// cosmic-text asserts (an uncatchable process abort) if font size OR
|
||||||
// line height is 0. `font-size:0` is a common whitespace-collapse trick
|
// line height is 0. `font-size:0` is a common whitespace-collapse trick
|
||||||
@@ -1298,7 +1365,8 @@ impl TextEngine {
|
|||||||
|
|
||||||
let idx = self.items.len();
|
let idx = self.items.len();
|
||||||
let text_indent = base.text_indent.unwrap_or(Dimension::Px(0.0));
|
let text_indent = base.text_indent.unwrap_or(Dimension::Px(0.0));
|
||||||
let source_buffer = (!matches!(text_indent, Dimension::Px(value) if value == 0.0))
|
let source_buffer = (!matches!(text_indent, Dimension::Px(value) if value == 0.0)
|
||||||
|
|| !boundary_events.is_empty())
|
||||||
.then(|| buffer.clone());
|
.then(|| buffer.clone());
|
||||||
self.items.push(InlineItem {
|
self.items.push(InlineItem {
|
||||||
buffer,
|
buffer,
|
||||||
@@ -1320,6 +1388,8 @@ impl TextEngine {
|
|||||||
marker: None,
|
marker: None,
|
||||||
owner_text,
|
owner_text,
|
||||||
owner_ranges,
|
owner_ranges,
|
||||||
|
owner_boxes,
|
||||||
|
boundary_events,
|
||||||
relative_owner_ranges: Vec::new(),
|
relative_owner_ranges: Vec::new(),
|
||||||
});
|
});
|
||||||
Some(idx)
|
Some(idx)
|
||||||
@@ -1347,8 +1417,7 @@ impl TextEngine {
|
|||||||
let TextEngine { font_system, items, .. } = self;
|
let TextEngine { font_system, items, .. } = self;
|
||||||
let item = &mut items[idx];
|
let item = &mut items[idx];
|
||||||
shape_with_text_indent(font_system, item, width, wrap);
|
shape_with_text_indent(font_system, item, width, wrap);
|
||||||
let (width, height, clamped) =
|
let (width, height, clamped) = buffer_size(item);
|
||||||
buffer_size(&item.buffer, item.first_line_offset, item.line_clamp);
|
|
||||||
(
|
(
|
||||||
width,
|
width,
|
||||||
if clamped {
|
if clamped {
|
||||||
@@ -1374,13 +1443,22 @@ impl TextEngine {
|
|||||||
shape_with_text_indent(font_system, item, None, wrap);
|
shape_with_text_indent(font_system, item, None, wrap);
|
||||||
let mut width = 0.0f32;
|
let mut width = 0.0f32;
|
||||||
let mut height = 0.0f32;
|
let mut height = 0.0f32;
|
||||||
|
let starts = item
|
||||||
|
.owner_text
|
||||||
|
.as_deref()
|
||||||
|
.map(|source| source_line_starts(&item.buffer, source))
|
||||||
|
.unwrap_or_default();
|
||||||
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
||||||
let offset = if line_index == 0 {
|
let offset = if line_index == 0 {
|
||||||
item.first_line_offset
|
item.first_line_offset
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
width = width.max((run.line_w + offset).max(0.0));
|
let line_start = starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
|
let line_end = line_start + run.text.len();
|
||||||
|
width = width.max(
|
||||||
|
(run.line_w + offset + line_edge_advance(item, line_start, line_end)).max(0.0),
|
||||||
|
);
|
||||||
height = height.max(run.line_top + run.line_height);
|
height = height.max(run.line_top + run.line_height);
|
||||||
}
|
}
|
||||||
(width, height.max(item.forced_min_height))
|
(width, height.max(item.forced_min_height))
|
||||||
@@ -1540,7 +1618,7 @@ impl TextEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_inline_owners(&self) -> bool {
|
pub(crate) fn has_inline_owners(&self) -> bool {
|
||||||
self.items.iter().any(|item| !item.owner_ranges.is_empty())
|
self.items.iter().any(|item| !item.owner_boxes.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Install cumulative used offsets for relative ordinary-inline owners.
|
/// Install cumulative used offsets for relative ordinary-inline owners.
|
||||||
@@ -1588,21 +1666,49 @@ impl TextEngine {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
for range in &item.owner_ranges {
|
let alignment_shift = line_edge_alignment_shift(item, line_start, line_end);
|
||||||
let start = range.start.max(line_start);
|
for owner in &item.owner_boxes {
|
||||||
let end = range.end.min(line_end);
|
let empty = owner.start == owner.end;
|
||||||
if start >= end {
|
let intersects = if empty {
|
||||||
|
(owner.start >= line_start && owner.start < line_end)
|
||||||
|
|| (line_end == source.len() && owner.start == line_end)
|
||||||
|
} else {
|
||||||
|
owner.start < line_end && owner.end > line_start
|
||||||
|
};
|
||||||
|
if !intersects {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let start = Cursor::new(run.line_i, start - line_start);
|
let first = owner.start >= line_start && owner.start < line_end || empty;
|
||||||
let end = Cursor::new(run.line_i, end - line_start);
|
let last = owner.end > line_start && owner.end <= line_end || empty;
|
||||||
let Some((x, width)) = run.highlight(start, end) else {
|
let raw_left = if first {
|
||||||
continue;
|
run_cursor_x(&run, owner.start.saturating_sub(line_start))
|
||||||
|
+ line_advance_before_event(
|
||||||
|
item,
|
||||||
|
owner.start_event,
|
||||||
|
line_start,
|
||||||
|
line_end,
|
||||||
|
)
|
||||||
|
+ owner.start_edge.margin
|
||||||
|
} else {
|
||||||
|
run_cursor_x(&run, 0)
|
||||||
};
|
};
|
||||||
let x = item.origin.0 + first_line_offset + x;
|
let raw_right = if last {
|
||||||
|
run_cursor_x(&run, owner.end.saturating_sub(line_start))
|
||||||
|
+ line_advance_before_event(
|
||||||
|
item,
|
||||||
|
owner.end_event,
|
||||||
|
line_start,
|
||||||
|
line_end,
|
||||||
|
)
|
||||||
|
+ owner.end_edge.border_padding()
|
||||||
|
} else {
|
||||||
|
run.line_w + line_edge_advance(item, line_start, line_end)
|
||||||
|
};
|
||||||
|
let x = item.origin.0 + first_line_offset + alignment_shift + raw_left;
|
||||||
|
let width = (raw_right - raw_left).max(0.0);
|
||||||
let baseline_y = item.origin.1 + run.line_y;
|
let baseline_y = item.origin.1 + run.line_y;
|
||||||
if let Some(existing) = out.iter_mut().find(|fragment| {
|
if let Some(existing) = out.iter_mut().find(|fragment| {
|
||||||
fragment.owner == range.owner
|
fragment.owner == owner.owner
|
||||||
&& fragment.item_index == item_index
|
&& fragment.item_index == item_index
|
||||||
&& fragment.line_index == line_index
|
&& fragment.line_index == line_index
|
||||||
}) {
|
}) {
|
||||||
@@ -1612,7 +1718,7 @@ impl TextEngine {
|
|||||||
existing.width = (right - left).max(0.0);
|
existing.width = (right - left).max(0.0);
|
||||||
} else {
|
} else {
|
||||||
out.push(InlineOwnerLineFragment {
|
out.push(InlineOwnerLineFragment {
|
||||||
owner: range.owner,
|
owner: owner.owner,
|
||||||
item_index,
|
item_index,
|
||||||
line_index,
|
line_index,
|
||||||
x,
|
x,
|
||||||
@@ -1770,8 +1876,10 @@ struct SpanCtx {
|
|||||||
struct Collector {
|
struct Collector {
|
||||||
last_was_space: bool,
|
last_was_space: bool,
|
||||||
clip_fills: Vec<ClipTextFill>,
|
clip_fills: Vec<ClipTextFill>,
|
||||||
owners: Vec<NodeId>,
|
owners: Vec<ActiveInlineOwner>,
|
||||||
owner_ranges: Vec<OwnerTextRange>,
|
owner_ranges: Vec<OwnerTextRange>,
|
||||||
|
owner_boxes: Vec<InlineOwnerBox>,
|
||||||
|
boundary_events: Vec<InlineBoundaryEvent>,
|
||||||
text_len: usize,
|
text_len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1782,6 +1890,8 @@ impl Collector {
|
|||||||
clip_fills: Vec::new(),
|
clip_fills: Vec::new(),
|
||||||
owners: Vec::new(),
|
owners: Vec::new(),
|
||||||
owner_ranges: Vec::new(),
|
owner_ranges: Vec::new(),
|
||||||
|
owner_boxes: Vec::new(),
|
||||||
|
boundary_events: Vec::new(),
|
||||||
text_len: 0,
|
text_len: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1789,14 +1899,62 @@ impl Collector {
|
|||||||
fn record_text(&mut self, byte_len: usize) {
|
fn record_text(&mut self, byte_len: usize) {
|
||||||
let start = self.text_len;
|
let start = self.text_len;
|
||||||
self.text_len = self.text_len.saturating_add(byte_len);
|
self.text_len = self.text_len.saturating_add(byte_len);
|
||||||
for &owner in &self.owners {
|
for owner in &self.owners {
|
||||||
self.owner_ranges.push(OwnerTextRange {
|
self.owner_ranges.push(OwnerTextRange {
|
||||||
owner,
|
owner: owner.owner,
|
||||||
start,
|
start,
|
||||||
end: self.text_len,
|
end: self.text_len,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn begin_owner(&mut self, owner: NodeId, style: &LayoutStyle) {
|
||||||
|
let start_edge = InlineEdge {
|
||||||
|
margin: style.margin.left,
|
||||||
|
border: style.border.left,
|
||||||
|
padding: style.padding.left,
|
||||||
|
};
|
||||||
|
let end_edge = InlineEdge {
|
||||||
|
margin: style.margin.right,
|
||||||
|
border: style.border.right,
|
||||||
|
padding: style.padding.right,
|
||||||
|
};
|
||||||
|
let start_event = self.boundary_events.len();
|
||||||
|
self.boundary_events.push(InlineBoundaryEvent {
|
||||||
|
owner,
|
||||||
|
position: self.text_len,
|
||||||
|
is_start: true,
|
||||||
|
edge: start_edge,
|
||||||
|
});
|
||||||
|
self.owners.push(ActiveInlineOwner {
|
||||||
|
owner,
|
||||||
|
start: self.text_len,
|
||||||
|
start_edge,
|
||||||
|
end_edge,
|
||||||
|
start_event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_owner(&mut self, owner: NodeId) {
|
||||||
|
let active = self.owners.pop().expect("balanced inline owner stack");
|
||||||
|
debug_assert_eq!(active.owner, owner);
|
||||||
|
let end_event = self.boundary_events.len();
|
||||||
|
self.boundary_events.push(InlineBoundaryEvent {
|
||||||
|
owner,
|
||||||
|
position: self.text_len,
|
||||||
|
is_start: false,
|
||||||
|
edge: active.end_edge,
|
||||||
|
});
|
||||||
|
self.owner_boxes.push(InlineOwnerBox {
|
||||||
|
owner,
|
||||||
|
start: active.start,
|
||||||
|
end: self.text_len,
|
||||||
|
start_edge: active.start_edge,
|
||||||
|
end_edge: active.end_edge,
|
||||||
|
start_event: active.start_event,
|
||||||
|
end_event,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// DFS the inline subtree, appending whitespace-collapsed text runs. Adjacent
|
/// DFS the inline subtree, appending whitespace-collapsed text runs. Adjacent
|
||||||
@@ -1966,7 +2124,7 @@ fn collect_node_spans(
|
|||||||
style.ignores_used_box_sizes() && !style.display_contents
|
style.ignores_used_box_sizes() && !style.display_contents
|
||||||
});
|
});
|
||||||
if owns_inline_fragment {
|
if owns_inline_fragment {
|
||||||
c.owners.push(cid);
|
c.begin_owner(cid, style.expect("inline owner style"));
|
||||||
}
|
}
|
||||||
let own_clip_fill = style.and_then(clip_text_fill).map(|fill| {
|
let own_clip_fill = style.and_then(clip_text_fill).map(|fill| {
|
||||||
let index = c.clip_fills.len();
|
let index = c.clip_fills.len();
|
||||||
@@ -2051,8 +2209,7 @@ fn collect_node_spans(
|
|||||||
loaded_families,
|
loaded_families,
|
||||||
);
|
);
|
||||||
if owns_inline_fragment {
|
if owns_inline_fragment {
|
||||||
let popped = c.owners.pop();
|
c.end_owner(cid);
|
||||||
debug_assert_eq!(popped, Some(cid));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2109,32 +2266,104 @@ fn push_text(
|
|||||||
out.push((buf, attrs.clone()));
|
out.push((buf, attrs.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn boundary_event_on_line(
|
||||||
|
item: &InlineItem,
|
||||||
|
event: &InlineBoundaryEvent,
|
||||||
|
line_start: usize,
|
||||||
|
line_end: usize,
|
||||||
|
) -> bool {
|
||||||
|
let source_end = item.owner_text.as_ref().map_or(0, String::len);
|
||||||
|
let empty = item
|
||||||
|
.owner_boxes
|
||||||
|
.iter()
|
||||||
|
.find(|owner| owner.owner == event.owner)
|
||||||
|
.is_some_and(|owner| owner.start == owner.end);
|
||||||
|
if empty {
|
||||||
|
(event.position >= line_start && event.position < line_end)
|
||||||
|
|| (line_end == source_end && event.position == line_end)
|
||||||
|
} else if event.is_start {
|
||||||
|
event.position >= line_start && event.position < line_end
|
||||||
|
} else {
|
||||||
|
event.position > line_start && event.position <= line_end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_edge_advance(item: &InlineItem, line_start: usize, line_end: usize) -> f32 {
|
||||||
|
item.boundary_events
|
||||||
|
.iter()
|
||||||
|
.filter(|event| boundary_event_on_line(item, event, line_start, line_end))
|
||||||
|
.map(|event| event.edge.advance())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_edge_alignment_shift(item: &InlineItem, line_start: usize, line_end: usize) -> f32 {
|
||||||
|
-line_edge_advance(item, line_start, line_end)
|
||||||
|
* match item.align {
|
||||||
|
Some(Align::Center) => 0.5,
|
||||||
|
Some(Align::End | Align::Right) => 1.0,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_advance_before_event(
|
||||||
|
item: &InlineItem,
|
||||||
|
event_index: usize,
|
||||||
|
line_start: usize,
|
||||||
|
line_end: usize,
|
||||||
|
) -> f32 {
|
||||||
|
item.boundary_events[..event_index]
|
||||||
|
.iter()
|
||||||
|
.filter(|event| boundary_event_on_line(item, event, line_start, line_end))
|
||||||
|
.map(|event| event.edge.advance())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_advance_before_text(
|
||||||
|
item: &InlineItem,
|
||||||
|
global_position: usize,
|
||||||
|
line_start: usize,
|
||||||
|
line_end: usize,
|
||||||
|
) -> f32 {
|
||||||
|
item.boundary_events
|
||||||
|
.iter()
|
||||||
|
.filter(|event| {
|
||||||
|
boundary_event_on_line(item, event, line_start, line_end)
|
||||||
|
&& event.position <= global_position
|
||||||
|
})
|
||||||
|
.map(|event| event.edge.advance())
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
/// Total shaped size of a buffer: widest line, and the bottom of the last line.
|
/// Total shaped size of a buffer: widest line, and the bottom of the last line.
|
||||||
fn buffer_size(
|
fn buffer_size(item: &InlineItem) -> (f32, f32, bool) {
|
||||||
buffer: &Buffer,
|
|
||||||
first_line_offset: f32,
|
|
||||||
line_clamp: Option<usize>,
|
|
||||||
) -> (f32, f32, bool) {
|
|
||||||
let mut w = 0.0f32;
|
let mut w = 0.0f32;
|
||||||
let mut h = 0.0f32;
|
let mut h = 0.0f32;
|
||||||
let mut nonempty_lines = 0usize;
|
let mut nonempty_lines = 0usize;
|
||||||
let mut clamp_height = None;
|
let mut clamp_height = None;
|
||||||
for (line_index, run) in buffer.layout_runs().enumerate() {
|
let line_starts = item
|
||||||
|
.owner_text
|
||||||
|
.as_deref()
|
||||||
|
.map(|source| source_line_starts(&item.buffer, source))
|
||||||
|
.unwrap_or_default();
|
||||||
|
for (line_index, run) in item.buffer.layout_runs().enumerate() {
|
||||||
let offset = if line_index == 0 {
|
let offset = if line_index == 0 {
|
||||||
first_line_offset
|
item.first_line_offset
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
w = w.max((run.line_w + offset).max(0.0));
|
let line_start = line_starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
|
let line_end = line_start + run.text.len();
|
||||||
|
let edges = line_edge_advance(item, line_start, line_end);
|
||||||
|
w = w.max((run.line_w + offset + edges).max(0.0));
|
||||||
h = h.max(run.line_top + run.line_height);
|
h = h.max(run.line_top + run.line_height);
|
||||||
if !run.glyphs.is_empty() {
|
if !run.glyphs.is_empty() {
|
||||||
nonempty_lines += 1;
|
nonempty_lines += 1;
|
||||||
if line_clamp == Some(nonempty_lines) {
|
if item.line_clamp == Some(nonempty_lines) {
|
||||||
clamp_height = Some(run.line_top + run.line_height);
|
clamp_height = Some(run.line_top + run.line_height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let clamped = line_clamp.is_some_and(|limit| nonempty_lines > limit);
|
let clamped = item.line_clamp.is_some_and(|limit| nonempty_lines > limit);
|
||||||
(
|
(
|
||||||
w.ceil(),
|
w.ceil(),
|
||||||
if clamped { clamp_height.unwrap_or(h) } else { h },
|
if clamped { clamp_height.unwrap_or(h) } else { h },
|
||||||
@@ -2170,6 +2399,20 @@ fn source_line_starts(buffer: &Buffer, source: &str) -> Vec<usize> {
|
|||||||
starts
|
starts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_cursor_x(run: &cosmic_text::LayoutRun<'_>, byte: usize) -> f32 {
|
||||||
|
let cursor = Cursor::new(run.line_i, byte.min(run.text.len()));
|
||||||
|
if let Some((x, _)) = run.highlight(cursor, cursor) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
if byte == 0 {
|
||||||
|
run.glyphs.first().map_or(0.0, |glyph| glyph.x)
|
||||||
|
} else {
|
||||||
|
run.glyphs
|
||||||
|
.last()
|
||||||
|
.map_or(run.line_w, |glyph| glyph.x + glyph.w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn glyph_relative_offset(
|
fn glyph_relative_offset(
|
||||||
ranges: &[RelativeOwnerTextRange],
|
ranges: &[RelativeOwnerTextRange],
|
||||||
line_start: usize,
|
line_start: usize,
|
||||||
@@ -2197,13 +2440,14 @@ fn used_text_indent(value: Dimension, width: Option<f32>) -> f32 {
|
|||||||
if indent.is_finite() { indent } else { 0.0 }
|
if indent.is_finite() { indent } else { 0.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shape one IFC with a first-line-only indent.
|
/// Shape one IFC with first-line indent and ordinary-inline boundary advances.
|
||||||
///
|
///
|
||||||
/// cosmic-text accepts one wrap width for every visual line. To retain the
|
/// The pristine buffer remains one shaping stream across DOM owners. For a
|
||||||
/// browser model, first probe the pristine paragraph at `width - indent`, take
|
/// definite width we probe cosmic-text's own UAX/CSS wrap boundary, subtract
|
||||||
/// its first wrap boundary, split there, and reshape the tail at the full
|
/// the ordered margin/border/padding events that fall on that candidate line,
|
||||||
/// content width. The synthetic split lives only in the working clone, so
|
/// and monotonically retry if those edges force an earlier break. Only final
|
||||||
/// repeated intrinsic/final Taffy measurements never accumulate hard breaks.
|
/// visual-line boundaries split BufferLines, so ligatures and kerning are not
|
||||||
|
/// broken merely because an inline element starts or ends.
|
||||||
fn shape_with_text_indent(
|
fn shape_with_text_indent(
|
||||||
font_system: &mut FontSystem,
|
font_system: &mut FontSystem,
|
||||||
item: &mut InlineItem,
|
item: &mut InlineItem,
|
||||||
@@ -2228,34 +2472,113 @@ fn shape_with_text_indent(
|
|||||||
item.buffer = source.clone();
|
item.buffer = source.clone();
|
||||||
item.buffer.set_wrap(font_system, wrap);
|
item.buffer.set_wrap(font_system, wrap);
|
||||||
|
|
||||||
let mut first_break = None;
|
if let (Some(full_width), Some(source_text)) = (width, item.owner_text.as_deref()) {
|
||||||
if let Some(full_width) = width {
|
let metrics = item.buffer.metrics();
|
||||||
|
let mono = item.buffer.monospace_width();
|
||||||
|
let tab_width = item.buffer.tab_width();
|
||||||
|
let mut line_index = 0usize;
|
||||||
|
while line_index < item.buffer.lines.len() {
|
||||||
|
let starts = source_line_starts(&item.buffer, source_text);
|
||||||
|
let global_start = starts.get(line_index).copied().unwrap_or(0);
|
||||||
|
let first_indent = if line_index == 0 { indent } else { 0.0 };
|
||||||
|
let base_available = (full_width - first_indent).max(0.0);
|
||||||
|
// Negative inline margins can admit content that would not fit in
|
||||||
|
// the text-only probe. Begin at the widest possible candidate and
|
||||||
|
// retain the same monotonic-decrease convergence used for positive
|
||||||
|
// padding/border advances.
|
||||||
|
let line_source_end = global_start + item.buffer.lines[line_index].text().len();
|
||||||
|
let negative_edges = item
|
||||||
|
.boundary_events
|
||||||
|
.iter()
|
||||||
|
.filter(|event| {
|
||||||
|
event.position >= global_start && event.position <= line_source_end
|
||||||
|
})
|
||||||
|
.map(|event| event.edge.advance().min(0.0))
|
||||||
|
.sum::<f32>();
|
||||||
|
let mut available = (base_available - negative_edges).max(0.0);
|
||||||
|
let mut split = None;
|
||||||
|
|
||||||
|
for _ in 0..=item.boundary_events.len() {
|
||||||
|
let (candidate, line_width) = {
|
||||||
|
let line = &mut item.buffer.lines[line_index];
|
||||||
|
let layouts = line.layout(
|
||||||
|
font_system,
|
||||||
|
metrics.font_size,
|
||||||
|
Some(available),
|
||||||
|
wrap,
|
||||||
|
mono,
|
||||||
|
tab_width,
|
||||||
|
);
|
||||||
|
let Some(first) = layouts.first() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
(
|
||||||
|
first.glyphs.iter().map(|glyph| glyph.end).max(),
|
||||||
|
first.w,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let Some(candidate) = candidate else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let candidate_end = global_start.saturating_add(candidate);
|
||||||
|
let edges = line_edge_advance(item, global_start, candidate_end);
|
||||||
|
let required_available = (base_available - edges).max(0.0);
|
||||||
|
split = Some(candidate);
|
||||||
|
if line_width + edges <= base_available + 0.01
|
||||||
|
|| required_available + 0.01 >= available
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
available = required_available;
|
||||||
|
item.buffer.lines[line_index].reset_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(mut split) = split else {
|
||||||
|
line_index += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let text = item.buffer.lines[line_index].text();
|
||||||
|
while split < text.len() {
|
||||||
|
let Some(ch) = text[split..].chars().next() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if !ch.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
split += ch.len_utf8();
|
||||||
|
}
|
||||||
|
if split > 0 && split < text.len() {
|
||||||
|
let tail = item.buffer.lines[line_index].split_off(split);
|
||||||
|
item.buffer.lines.insert(line_index + 1, tail);
|
||||||
|
}
|
||||||
|
line_index += 1;
|
||||||
|
}
|
||||||
|
} else if let Some(full_width) = width {
|
||||||
item.buffer
|
item.buffer
|
||||||
.set_size(font_system, Some((full_width - indent).max(0.0)), None);
|
.set_size(font_system, Some((full_width - indent).max(0.0)), None);
|
||||||
item.buffer.shape_until_scroll(font_system, false);
|
item.buffer.shape_until_scroll(font_system, false);
|
||||||
first_break = item
|
let first_break = item
|
||||||
.buffer
|
.buffer
|
||||||
.layout_runs()
|
.layout_runs()
|
||||||
.next()
|
.next()
|
||||||
.and_then(|run| run.glyphs.iter().map(|glyph| glyph.end).max());
|
.and_then(|run| run.glyphs.iter().map(|glyph| glyph.end).max());
|
||||||
}
|
item.buffer = source.clone();
|
||||||
|
item.buffer.set_wrap(font_system, wrap);
|
||||||
item.buffer = source.clone();
|
if let (Some(mut split), Some(first_line)) = (first_break, item.buffer.lines.first()) {
|
||||||
item.buffer.set_wrap(font_system, wrap);
|
let text = first_line.text();
|
||||||
if let (Some(mut split), Some(first_line)) =
|
while split < text.len() {
|
||||||
(first_break, item.buffer.lines.first())
|
let Some(ch) = text[split..].chars().next() else {
|
||||||
{
|
break;
|
||||||
let text = first_line.text();
|
};
|
||||||
while split < text.len() {
|
if !ch.is_whitespace() {
|
||||||
let Some(ch) = text[split..].chars().next() else { break };
|
break;
|
||||||
if !ch.is_whitespace() {
|
}
|
||||||
break;
|
split += ch.len_utf8();
|
||||||
|
}
|
||||||
|
if split > 0 && split < text.len() {
|
||||||
|
let tail = item.buffer.lines[0].split_off(split);
|
||||||
|
item.buffer.lines.insert(1, tail);
|
||||||
}
|
}
|
||||||
split += ch.len_utf8();
|
|
||||||
}
|
|
||||||
if split > 0 && split < text.len() {
|
|
||||||
let tail = item.buffer.lines[0].split_off(split);
|
|
||||||
item.buffer.lines.insert(1, tail);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item.buffer
|
item.buffer
|
||||||
@@ -2451,6 +2774,12 @@ fn inline_child_ok(tree: &DomTree, cid: NodeId, styles: &std::collections::HashM
|
|||||||
if !foldable_inline {
|
if !foldable_inline {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if style.margin != crate::Edges::default()
|
||||||
|
|| style.padding != crate::Edges::default()
|
||||||
|
|| style.border != crate::Edges::default()
|
||||||
|
{
|
||||||
|
*has_text = true;
|
||||||
|
}
|
||||||
for gc in crate::dom::rendered_children(tree, cid) {
|
for gc in crate::dom::rendered_children(tree, cid) {
|
||||||
if !inline_child_ok(tree, gc, styles, has_text) {
|
if !inline_child_ok(tree, gc, styles, has_text) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2572,7 +2901,9 @@ impl TextEngine {
|
|||||||
let line_source_starts = item
|
let line_source_starts = item
|
||||||
.owner_text
|
.owner_text
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.filter(|_| !item.relative_owner_ranges.is_empty())
|
.filter(|_| {
|
||||||
|
!item.relative_owner_ranges.is_empty() || !item.boundary_events.is_empty()
|
||||||
|
})
|
||||||
.map(|source| source_line_starts(&item.buffer, source))
|
.map(|source| source_line_starts(&item.buffer, source))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -2590,6 +2921,9 @@ impl TextEngine {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
|
let line_source_end = line_source_start + run.text.len();
|
||||||
|
let inline_alignment =
|
||||||
|
line_edge_alignment_shift(item, line_source_start, line_source_end);
|
||||||
let base_y = run.line_y;
|
let base_y = run.line_y;
|
||||||
let mut seg: Option<(f32, f32, f32, [u8; 4], (f32, f32))> = None;
|
let mut seg: Option<(f32, f32, f32, [u8; 4], (f32, f32))> = None;
|
||||||
for g in run.glyphs {
|
for g in run.glyphs {
|
||||||
@@ -2603,12 +2937,19 @@ impl TextEngine {
|
|||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let relative = glyph_relative_offset(
|
let mut relative = glyph_relative_offset(
|
||||||
&item.relative_owner_ranges,
|
&item.relative_owner_ranges,
|
||||||
line_source_start,
|
line_source_start,
|
||||||
g.start,
|
g.start,
|
||||||
g.end,
|
g.end,
|
||||||
);
|
);
|
||||||
|
relative.0 += inline_alignment
|
||||||
|
+ line_advance_before_text(
|
||||||
|
item,
|
||||||
|
line_source_start + g.start,
|
||||||
|
line_source_start,
|
||||||
|
line_source_end,
|
||||||
|
);
|
||||||
let underlined = g.metadata & META_UNDERLINE != 0;
|
let underlined = g.metadata & META_UNDERLINE != 0;
|
||||||
if let Some(fill_index) = metadata_fill(g.metadata) {
|
if let Some(fill_index) = metadata_fill(g.metadata) {
|
||||||
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
if let Some(bounds) = fill_bounds.get_mut(fill_index) {
|
||||||
@@ -2677,6 +3018,9 @@ impl TextEngine {
|
|||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
let line_source_start = line_source_starts.get(run.line_i).copied().unwrap_or(0);
|
||||||
|
let line_source_end = line_source_start + run.text.len();
|
||||||
|
let inline_alignment =
|
||||||
|
line_edge_alignment_shift(item, line_source_start, line_source_end);
|
||||||
for glyph in run.glyphs {
|
for glyph in run.glyphs {
|
||||||
if item.marker.is_some_and(|marker| {
|
if item.marker.is_some_and(|marker| {
|
||||||
marker.line_index == line_index
|
marker.line_index == line_index
|
||||||
@@ -2684,12 +3028,19 @@ impl TextEngine {
|
|||||||
}) {
|
}) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let relative = glyph_relative_offset(
|
let mut relative = glyph_relative_offset(
|
||||||
&item.relative_owner_ranges,
|
&item.relative_owner_ranges,
|
||||||
line_source_start,
|
line_source_start,
|
||||||
glyph.start,
|
glyph.start,
|
||||||
glyph.end,
|
glyph.end,
|
||||||
);
|
);
|
||||||
|
relative.0 += inline_alignment
|
||||||
|
+ line_advance_before_text(
|
||||||
|
item,
|
||||||
|
line_source_start + glyph.start,
|
||||||
|
line_source_start,
|
||||||
|
line_source_end,
|
||||||
|
);
|
||||||
let physical = glyph.physical(
|
let physical = glyph.physical(
|
||||||
(line_offset + relative.0, relative.1),
|
(line_offset + relative.0, relative.1),
|
||||||
1.0,
|
1.0,
|
||||||
|
|||||||
@@ -1694,14 +1694,45 @@ fn paint_laid_dom_scrolled(
|
|||||||
viewport,
|
viewport,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
let has_background_box_paint = !style.background_clip_text
|
||||||
|
&& (style.background_color.is_some()
|
||||||
|
|| style.background_image.is_some()
|
||||||
|
|| style.mask_image.is_some()
|
||||||
|
|| style.background_gradient.is_some()
|
||||||
|
|| style.background_radial_gradient.is_some()
|
||||||
|
|| style.background_conic_gradient.is_some()
|
||||||
|
|| !style.background_gradient_layers.is_empty());
|
||||||
|
let has_inline_box_paint = style.box_shadow.is_some()
|
||||||
|
|| has_background_box_paint
|
||||||
|
|| style.border != crate::Edges::default();
|
||||||
|
let inline_pieces = (style.ignores_used_box_sizes() && has_inline_box_paint)
|
||||||
|
.then(|| laid.inline_fragments.get(&nid))
|
||||||
|
.flatten();
|
||||||
|
let paints_inline_fragments = inline_pieces.is_some();
|
||||||
|
if let Some(pieces) = inline_pieces {
|
||||||
|
paint_inline_fragment_decorations(
|
||||||
|
&mut pixmap,
|
||||||
|
pieces,
|
||||||
|
(ox, oy),
|
||||||
|
&rect,
|
||||||
|
style,
|
||||||
|
clip,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
base_url,
|
||||||
|
image_cache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Outset box-shadow paints behind this element's own background/border.
|
// Outset box-shadow paints behind this element's own background/border.
|
||||||
// Geometry comes from the full (translate-adjusted) border box; the
|
// Geometry comes from the full (translate-adjusted) border box; the
|
||||||
// ancestor overflow clip is reapplied inside so the shadow is clipped by
|
// ancestor overflow clip is reapplied inside so the shadow is clipped by
|
||||||
// an ancestor exactly as the box itself is.
|
// an ancestor exactly as the box itself is.
|
||||||
|
if !paints_inline_fragments {
|
||||||
if let Some(shadow) = style.box_shadow {
|
if let Some(shadow) = style.box_shadow {
|
||||||
paint_box_shadow(&mut pixmap, &shadow, &rect, style.border_model.radii, clip);
|
paint_box_shadow(&mut pixmap, &shadow, &rect, style.border_model.radii, clip);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Box path (rounded if border-radius), reused for gradient/color fill.
|
// Box path (rounded if border-radius), reused for gradient/color fill.
|
||||||
let radius = style.border_model.radii.resolve(rect.width, rect.height);
|
let radius = style.border_model.radii.resolve(rect.width, rect.height);
|
||||||
@@ -1725,7 +1756,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
// without this it paints white. Takes precedence over a solid color.
|
// without this it paints white. Takes precedence over a solid color.
|
||||||
// `background-clip: text` clips the background to the glyphs, so it must
|
// `background-clip: text` clips the background to the glyphs, so it must
|
||||||
// not paint as a box here; the text paint path fills the glyphs instead.
|
// not paint as a box here; the text paint path fills the glyphs instead.
|
||||||
if style.mask_image.is_none() && !style.background_clip_text {
|
if !paints_inline_fragments && style.mask_image.is_none() && !style.background_clip_text {
|
||||||
if let Some(bg) = style.background_color {
|
if let Some(bg) = style.background_color {
|
||||||
if let Some(path) = bg_path() {
|
if let Some(path) = bg_path() {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
@@ -1792,6 +1823,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !paints_inline_fragments {
|
||||||
if let Some(mask_url) = &style.mask_image {
|
if let Some(mask_url) = &style.mask_image {
|
||||||
let fill = style.background_color.or(style.color).unwrap_or([0, 0, 0, 255]);
|
let fill = style.background_color.or(style.color).unwrap_or([0, 0, 0, 255]);
|
||||||
paint_mask(
|
paint_mask(
|
||||||
@@ -1847,6 +1879,7 @@ fn paint_laid_dom_scrolled(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let positioned_pseudo_containing_block = {
|
let positioned_pseudo_containing_block = {
|
||||||
let mut ancestor = Some(nid);
|
let mut ancestor = Some(nid);
|
||||||
let mut found = None;
|
let mut found = None;
|
||||||
@@ -1903,12 +1936,14 @@ fn paint_laid_dom_scrolled(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
paint_css_border(
|
if !paints_inline_fragments {
|
||||||
&mut pixmap,
|
paint_css_border(
|
||||||
&rect,
|
&mut pixmap,
|
||||||
style,
|
&rect,
|
||||||
clip_path_mask.as_ref(),
|
style,
|
||||||
);
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
paint_css_outline(
|
paint_css_outline(
|
||||||
&mut pixmap,
|
&mut pixmap,
|
||||||
&rect,
|
&rect,
|
||||||
@@ -2224,6 +2259,224 @@ fn paint_laid_dom_scrolled(
|
|||||||
Some(pixmap)
|
Some(pixmap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Paint an ordinary inline's sliced border boxes instead of its multiline
|
||||||
|
/// bounding union. Background coordinates stay anchored to the union so
|
||||||
|
/// gradients and images remain one joined `box-decoration-break:slice`
|
||||||
|
/// decoration, while left/right borders and radii appear only on the first
|
||||||
|
/// and last continuation respectively.
|
||||||
|
fn paint_inline_fragment_decorations(
|
||||||
|
pixmap: &mut Pixmap,
|
||||||
|
fragments: &[crate::Rect],
|
||||||
|
offset: (f32, f32),
|
||||||
|
union: &crate::Rect,
|
||||||
|
style: &crate::LayoutStyle,
|
||||||
|
clip: Option<crate::Rect>,
|
||||||
|
root_font_size: f32,
|
||||||
|
viewport: (f32, f32),
|
||||||
|
base_url: Option<&str>,
|
||||||
|
image_cache: &mut RenderResourceCache,
|
||||||
|
) {
|
||||||
|
let union = crate::Rect {
|
||||||
|
x: union.x,
|
||||||
|
y: union.y,
|
||||||
|
width: union.width,
|
||||||
|
height: union.height,
|
||||||
|
};
|
||||||
|
let mut fragment_style = style.clone();
|
||||||
|
for (index, fragment) in fragments.iter().enumerate() {
|
||||||
|
let fragment = crate::Rect {
|
||||||
|
x: fragment.x + offset.0,
|
||||||
|
y: fragment.y + offset.1,
|
||||||
|
..*fragment
|
||||||
|
};
|
||||||
|
let visible = match clip {
|
||||||
|
Some(clip) => match fragment.intersect(&clip) {
|
||||||
|
Some(visible) => visible,
|
||||||
|
None => continue,
|
||||||
|
},
|
||||||
|
None => fragment,
|
||||||
|
};
|
||||||
|
let first = index == 0;
|
||||||
|
let last = index + 1 == fragments.len();
|
||||||
|
fragment_style.border.left = style.border.left;
|
||||||
|
fragment_style.border.right = style.border.right;
|
||||||
|
fragment_style.border_model.radii = style.border_model.radii;
|
||||||
|
if !first {
|
||||||
|
fragment_style.border.left = 0.0;
|
||||||
|
fragment_style.border_model.radii.top_left = crate::CornerRadius::default();
|
||||||
|
fragment_style.border_model.radii.bottom_left = crate::CornerRadius::default();
|
||||||
|
}
|
||||||
|
if !last {
|
||||||
|
fragment_style.border.right = 0.0;
|
||||||
|
fragment_style.border_model.radii.top_right = crate::CornerRadius::default();
|
||||||
|
fragment_style.border_model.radii.bottom_right = crate::CornerRadius::default();
|
||||||
|
}
|
||||||
|
let radius = fragment_style
|
||||||
|
.border_model
|
||||||
|
.radii
|
||||||
|
.resolve(fragment.width, fragment.height);
|
||||||
|
let has_radius = !radius.is_zero();
|
||||||
|
let clip_path_mask = fragment_style.clip_path.as_ref().and_then(|polygon| {
|
||||||
|
polygon_clip_mask(
|
||||||
|
pixmap.width(),
|
||||||
|
pixmap.height(),
|
||||||
|
polygon,
|
||||||
|
&fragment,
|
||||||
|
fragment_style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let path = if has_radius {
|
||||||
|
rounded_rect_path_radii(
|
||||||
|
visible.x,
|
||||||
|
visible.y,
|
||||||
|
visible.width,
|
||||||
|
visible.height,
|
||||||
|
radius,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Rect::from_xywh(visible.x, visible.y, visible.width, visible.height).and_then(
|
||||||
|
|rect| {
|
||||||
|
let mut builder = PathBuilder::new();
|
||||||
|
builder.push_rect(rect);
|
||||||
|
builder.finish()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(shadow) = fragment_style.box_shadow {
|
||||||
|
paint_box_shadow(
|
||||||
|
pixmap,
|
||||||
|
&shadow,
|
||||||
|
&fragment,
|
||||||
|
fragment_style.border_model.radii,
|
||||||
|
clip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if fragment_style.mask_image.is_none() && !fragment_style.background_clip_text {
|
||||||
|
if let (Some(background), Some(path)) = (fragment_style.background_color, path.as_ref()) {
|
||||||
|
let mut paint = Paint::default();
|
||||||
|
paint.set_color(Color::from_rgba8(
|
||||||
|
background[0],
|
||||||
|
background[1],
|
||||||
|
background[2],
|
||||||
|
background[3],
|
||||||
|
));
|
||||||
|
paint.anti_alias = has_radius;
|
||||||
|
pixmap.fill_path(
|
||||||
|
path,
|
||||||
|
&paint,
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !fragment_style.background_gradient_layers.is_empty() {
|
||||||
|
if let Some(path) = path.as_ref() {
|
||||||
|
paint_background_gradient_layers(
|
||||||
|
pixmap,
|
||||||
|
path,
|
||||||
|
&union,
|
||||||
|
radius,
|
||||||
|
&fragment_style,
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let (Some((center, stops)), Some(path)) =
|
||||||
|
(&fragment_style.background_radial_gradient, path.as_ref())
|
||||||
|
{
|
||||||
|
paint_radial_gradient(
|
||||||
|
pixmap,
|
||||||
|
path,
|
||||||
|
&union,
|
||||||
|
*center,
|
||||||
|
stops,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some((angle, center, stops)) = &fragment_style.background_conic_gradient {
|
||||||
|
paint_conic_gradient_sampled(
|
||||||
|
pixmap,
|
||||||
|
&fragment,
|
||||||
|
&union,
|
||||||
|
radius,
|
||||||
|
*angle,
|
||||||
|
*center,
|
||||||
|
stops,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let (Some((angle, stops)), Some(path)) =
|
||||||
|
(&fragment_style.background_gradient, path.as_ref())
|
||||||
|
{
|
||||||
|
paint_linear_gradient(
|
||||||
|
pixmap,
|
||||||
|
path,
|
||||||
|
&union,
|
||||||
|
*angle,
|
||||||
|
stops,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mask_url) = &fragment_style.mask_image {
|
||||||
|
let fill = fragment_style
|
||||||
|
.background_color
|
||||||
|
.or(fragment_style.color)
|
||||||
|
.unwrap_or([0, 0, 0, 255]);
|
||||||
|
paint_mask(
|
||||||
|
mask_url,
|
||||||
|
base_url,
|
||||||
|
&fragment,
|
||||||
|
radius,
|
||||||
|
fill,
|
||||||
|
fragment_style.background_radial_gradient.as_ref(),
|
||||||
|
fragment_style.background_gradient.as_ref(),
|
||||||
|
fragment_style.background_conic_gradient.as_ref(),
|
||||||
|
fragment_style.mask_size,
|
||||||
|
fragment_style.mask_repeat,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
pixmap,
|
||||||
|
image_cache,
|
||||||
|
);
|
||||||
|
} else if let Some(background_url) = &fragment_style.background_image {
|
||||||
|
if let Some(image_rect) = background_image_rect(
|
||||||
|
background_url,
|
||||||
|
base_url,
|
||||||
|
&union,
|
||||||
|
fragment_style.background_size,
|
||||||
|
fragment_style.background_size_expression.as_deref(),
|
||||||
|
fragment_style.background_size_fit,
|
||||||
|
fragment_style.background_position,
|
||||||
|
fragment_style.font_size.unwrap_or(16.0),
|
||||||
|
root_font_size,
|
||||||
|
viewport,
|
||||||
|
image_cache,
|
||||||
|
) {
|
||||||
|
paint_image(
|
||||||
|
background_url,
|
||||||
|
base_url,
|
||||||
|
&image_rect,
|
||||||
|
&visible,
|
||||||
|
crate::ObjectFit::Fill,
|
||||||
|
pixmap,
|
||||||
|
image_cache,
|
||||||
|
None,
|
||||||
|
radius,
|
||||||
|
clip_path_mask.as_ref(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paint_css_border(pixmap, &fragment, &fragment_style, clip_path_mask.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-capture root-scroll and sticky offsets layered over an immutable
|
/// Per-capture root-scroll and sticky offsets layered over an immutable
|
||||||
/// document-space [`DomLayout`]. Keeping these deltas out of the layout avoids
|
/// document-space [`DomLayout`]. Keeping these deltas out of the layout avoids
|
||||||
/// accumulating movement when the same prepared document paints more than one
|
/// accumulating movement when the same prepared document paints more than one
|
||||||
@@ -4309,6 +4562,28 @@ fn paint_conic_gradient(
|
|||||||
center: (f32, f32),
|
center: (f32, f32),
|
||||||
stops: &[([u8; 4], Option<f32>)],
|
stops: &[([u8; 4], Option<f32>)],
|
||||||
extra_clip: Option<&tiny_skia::Mask>,
|
extra_clip: Option<&tiny_skia::Mask>,
|
||||||
|
) {
|
||||||
|
paint_conic_gradient_sampled(
|
||||||
|
pixmap,
|
||||||
|
rect,
|
||||||
|
rect,
|
||||||
|
border_radius,
|
||||||
|
angle,
|
||||||
|
center,
|
||||||
|
stops,
|
||||||
|
extra_clip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_conic_gradient_sampled(
|
||||||
|
pixmap: &mut Pixmap,
|
||||||
|
rect: &crate::Rect,
|
||||||
|
sampling_rect: &crate::Rect,
|
||||||
|
border_radius: crate::ResolvedBorderRadii,
|
||||||
|
angle: f32,
|
||||||
|
center: (f32, f32),
|
||||||
|
stops: &[([u8; 4], Option<f32>)],
|
||||||
|
extra_clip: Option<&tiny_skia::Mask>,
|
||||||
) {
|
) {
|
||||||
if rect.width <= 0.0 || rect.height <= 0.0 || stops.len() < 2 {
|
if rect.width <= 0.0 || rect.height <= 0.0 || stops.len() < 2 {
|
||||||
return;
|
return;
|
||||||
@@ -4322,7 +4597,7 @@ fn paint_conic_gradient(
|
|||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let color = conic_color_at(
|
let color = conic_color_at(
|
||||||
rect,
|
sampling_rect,
|
||||||
angle,
|
angle,
|
||||||
center,
|
center,
|
||||||
&normalized,
|
&normalized,
|
||||||
@@ -8213,6 +8488,49 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrapped_inline_background_and_slice_borders_paint_per_continuation() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html,body,p { margin:0 }
|
||||||
|
p { width:70px; font:16px/24px monospace }
|
||||||
|
#token {
|
||||||
|
color:transparent; background:#ff0000; padding:0 10px;
|
||||||
|
border-left:2px solid #0000ff;
|
||||||
|
border-right:2px solid #0000ff
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p><span id="token">aaaa aaaa aaaa</span></p>"#,
|
||||||
|
);
|
||||||
|
let token = tree.get_element_by_id("token").unwrap();
|
||||||
|
let mut resources = RenderResourceCache::default();
|
||||||
|
let mut prepared = prepare_dom(&tree, (120.0, 100.0), None, &mut resources).unwrap();
|
||||||
|
let fragments = prepared.layout.inline_fragments[&token].clone();
|
||||||
|
assert_eq!(fragments.len(), 3, "{fragments:?}");
|
||||||
|
let pixmap = paint_prepared(&tree, &mut prepared, &mut resources, (0.0, 0.0)).unwrap();
|
||||||
|
let rgb = |x: f32, y: f32| {
|
||||||
|
let pixel = pixmap
|
||||||
|
.pixel(x.floor().max(0.0) as u32, y.floor().max(0.0) as u32)
|
||||||
|
.unwrap();
|
||||||
|
(pixel.red(), pixel.green(), pixel.blue())
|
||||||
|
};
|
||||||
|
let first = fragments[0];
|
||||||
|
let middle = fragments[1];
|
||||||
|
let last = fragments[2];
|
||||||
|
assert_eq!(rgb(first.x + 0.5, first.y + 1.0), (0, 0, 255));
|
||||||
|
assert_eq!(rgb(middle.x + 0.5, middle.y + 1.0), (255, 0, 0));
|
||||||
|
assert_eq!(
|
||||||
|
rgb(last.x + last.width - 0.5, last.y + 1.0),
|
||||||
|
(0, 0, 255)
|
||||||
|
);
|
||||||
|
let gap_y = (first.y + first.height + middle.y) * 0.5;
|
||||||
|
assert_eq!(
|
||||||
|
rgb(5.0, gap_y),
|
||||||
|
(255, 255, 255),
|
||||||
|
"the multiline bounding union must not fill the inter-line gap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overflow_clipping_keeps_physical_axes_independent() {
|
fn overflow_clipping_keeps_physical_axes_independent() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
Reference in new issue
Block a user