iris: clip ellipsized text at cluster boundaries
This commit is contained in:
1 parent
a475ae772f
commit
ab6d3fcdc8
1 file changed
+93
-22
+93
-22
@@ -26,6 +26,8 @@ struct OverflowState {
|
||||
pan: f32,
|
||||
leading_marker: f32,
|
||||
trailing_marker: f32,
|
||||
content_start: f32,
|
||||
content_end: f32,
|
||||
suppress_content: bool,
|
||||
leading_omitted: Option<OmittedText>,
|
||||
trailing_omitted: Option<OmittedText>,
|
||||
@@ -104,6 +106,45 @@ impl TextView {
|
||||
})
|
||||
}
|
||||
|
||||
/// Snaps a pair of canonical-layout clip edges outward from any shaped
|
||||
/// cluster or ligature they cross. The text itself stays at its exact
|
||||
/// (possibly animated) pan; only the opaque ellipsis boundary snaps.
|
||||
fn ellipsis_clip(&self, left: f32, right: f32) -> (f32, f32) {
|
||||
self.text.with_layout(|layout, _| {
|
||||
let y = self.overflow.viewport.y * 0.5;
|
||||
let cluster_group = |x| {
|
||||
let (mut first, _) = parley::Cluster::from_point(layout, x, y)?;
|
||||
while first.is_ligature_continuation() {
|
||||
first = first.previous_visual()?;
|
||||
}
|
||||
let start = first.visual_offset()?;
|
||||
let mut end = start + first.advance();
|
||||
let mut last = first;
|
||||
while let Some(next) = last.next_visual() {
|
||||
if !next.is_ligature_continuation() {
|
||||
break;
|
||||
}
|
||||
end += next.advance();
|
||||
last = next;
|
||||
}
|
||||
Some((start, end))
|
||||
};
|
||||
let left = cluster_group(left).map_or(
|
||||
left,
|
||||
|(start, end)| {
|
||||
if left <= start { start } else { end }
|
||||
},
|
||||
);
|
||||
let right = cluster_group(right).map_or(
|
||||
right,
|
||||
|(start, end)| {
|
||||
if right >= end { end } else { start }
|
||||
},
|
||||
);
|
||||
(left, right)
|
||||
})
|
||||
}
|
||||
|
||||
fn local_text_point(&self, pos: Vec2, size: Vec2) -> Vec2 {
|
||||
let mut pos = pos - self.region().top_left().to_abs(size);
|
||||
pos.x += self.overflow.pan;
|
||||
@@ -122,11 +163,9 @@ impl TextView {
|
||||
len
|
||||
},
|
||||
})
|
||||
} else if pos.x >= 0.0 && pos.x <= self.overflow.leading_marker {
|
||||
} else if pos.x >= 0.0 && pos.x <= self.overflow.content_start {
|
||||
self.overflow.leading_omitted.clone()
|
||||
} else if pos.x >= self.overflow.viewport.x - self.overflow.trailing_marker
|
||||
&& pos.x <= self.overflow.viewport.x
|
||||
{
|
||||
} else if pos.x >= self.overflow.content_end && pos.x <= self.overflow.viewport.x {
|
||||
self.overflow.trailing_omitted.clone()
|
||||
} else {
|
||||
None
|
||||
@@ -202,6 +241,7 @@ impl TextView {
|
||||
self.overflow.active = true;
|
||||
self.overflow.viewport = vec2(available, tex.size.y);
|
||||
self.overflow.pan = pan;
|
||||
self.overflow.content_end = available;
|
||||
if attrs.overflow != TextOverflow::Ellipsis {
|
||||
return None;
|
||||
}
|
||||
@@ -214,16 +254,23 @@ impl TextView {
|
||||
if markers_fit {
|
||||
self.overflow.leading_marker = if leading { marker.size.x } else { 0.0 };
|
||||
self.overflow.trailing_marker = if trailing { marker.size.x } else { 0.0 };
|
||||
let (clip_start, clip_end) = self.ellipsis_clip(
|
||||
pan + self.overflow.leading_marker,
|
||||
pan + available - self.overflow.trailing_marker,
|
||||
);
|
||||
if clip_start >= clip_end {
|
||||
self.overflow.suppress_content = true;
|
||||
return Some(marker);
|
||||
}
|
||||
self.overflow.content_start = (clip_start - pan).clamp(0.0, available);
|
||||
self.overflow.content_end = (clip_end - pan).clamp(0.0, available);
|
||||
if leading {
|
||||
self.overflow.leading_omitted =
|
||||
Some(self.omitted_range(-tex.size.x, pan + marker.size.x, -tex.size.x));
|
||||
Some(self.omitted_range(-tex.size.x, clip_start, -tex.size.x));
|
||||
}
|
||||
if trailing {
|
||||
self.overflow.trailing_omitted = Some(self.omitted_range(
|
||||
pan + available - marker.size.x,
|
||||
tex.size.x * 2.0,
|
||||
tex.size.x * 2.0,
|
||||
));
|
||||
self.overflow.trailing_omitted =
|
||||
Some(self.omitted_range(clip_end, tex.size.x * 2.0, tex.size.x * 2.0));
|
||||
}
|
||||
} else {
|
||||
self.overflow.suppress_content = true;
|
||||
@@ -266,7 +313,7 @@ impl TextView {
|
||||
.as_ref()
|
||||
.is_some_and(|omitted| self.omitted_is_selected(omitted))
|
||||
{
|
||||
let marker_region = vec2(self.overflow.leading_marker, self.overflow.viewport.y)
|
||||
let marker_region = vec2(self.overflow.content_start, self.overflow.viewport.y)
|
||||
.align(Align::TOP_LEFT)
|
||||
.within(&viewport)
|
||||
.within(&painter.region());
|
||||
@@ -278,12 +325,12 @@ impl TextView {
|
||||
.as_ref()
|
||||
.is_some_and(|omitted| self.omitted_is_selected(omitted))
|
||||
{
|
||||
let marker_region = vec2(self.overflow.trailing_marker, self.overflow.viewport.y)
|
||||
let marker_region = vec2(
|
||||
self.overflow.viewport.x - self.overflow.content_end,
|
||||
self.overflow.viewport.y,
|
||||
)
|
||||
.align(Align::TOP_LEFT)
|
||||
.offset(vec2(
|
||||
self.overflow.viewport.x - self.overflow.trailing_marker,
|
||||
0.0,
|
||||
))
|
||||
.offset(vec2(self.overflow.content_end, 0.0))
|
||||
.within(&viewport)
|
||||
.within(&painter.region());
|
||||
painter.primitive_within(RectPrimitive::color(marker_highlight), marker_region);
|
||||
@@ -305,14 +352,11 @@ impl TextView {
|
||||
let content_width = if self.overflow.suppress_content {
|
||||
0.0
|
||||
} else {
|
||||
(self.overflow.viewport.x
|
||||
- self.overflow.leading_marker
|
||||
- self.overflow.trailing_marker)
|
||||
.max(0.0)
|
||||
(self.overflow.content_end - self.overflow.content_start).max(0.0)
|
||||
};
|
||||
let mask = vec2(content_width, self.overflow.viewport.y)
|
||||
.align(Align::TOP_LEFT)
|
||||
.offset(vec2(self.overflow.leading_marker, 0.0))
|
||||
.offset(vec2(self.overflow.content_start, 0.0))
|
||||
.within(&viewport)
|
||||
.within(&painter.region());
|
||||
painter.set_mask(mask);
|
||||
@@ -357,7 +401,14 @@ impl TextView {
|
||||
.text
|
||||
.with_layout(|layout, _| selection.focus().geometry(layout, CARET_WIDTH));
|
||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let top_left = vec2(caret.x0 as f32 - self.overflow.pan, caret.y0 as f32);
|
||||
let mut x = caret.x0 as f32 - self.overflow.pan;
|
||||
if self.overflow.active && !self.overflow.suppress_content {
|
||||
x = x.clamp(
|
||||
self.overflow.content_start,
|
||||
(self.overflow.content_end - size.x).max(self.overflow.content_start),
|
||||
);
|
||||
}
|
||||
let top_left = vec2(x, caret.y0 as f32);
|
||||
let paint = painter.paint(&PaintId::WHITE);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(paint),
|
||||
@@ -577,6 +628,26 @@ mod tests {
|
||||
assert_eq!(render.take_counters().shapes, 0);
|
||||
assert!(rsc.ui.widgets[text].view.overflow.leading_marker > 0.0);
|
||||
assert!(rsc.ui.widgets[text].view.overflow.trailing_marker > 0.0);
|
||||
{
|
||||
let view = &rsc.ui.widgets[text].view;
|
||||
let edges = [
|
||||
view.overflow.pan + view.overflow.content_start,
|
||||
view.overflow.pan + view.overflow.content_end,
|
||||
];
|
||||
view.text.with_layout(|layout, _| {
|
||||
for edge in edges {
|
||||
let (cluster, _) =
|
||||
parley::Cluster::from_point(layout, edge, view.overflow.viewport.y * 0.5)
|
||||
.unwrap();
|
||||
let start = cluster.visual_offset().unwrap();
|
||||
let end = start + cluster.advance();
|
||||
assert!(
|
||||
(edge - start).abs() < 0.01 || (edge - end).abs() < 0.01,
|
||||
"ellipsis clip {edge} crossed cluster {start}..{end}",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rsc.ui.widgets[text].set_overflow_position(rel(1));
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
Reference in new issue
Block a user