mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 18:36:44 -06:00
Merge fix/671 into batch 13
This commit is contained in:
@@ -1191,6 +1191,12 @@ impl TextLayout {
|
||||
Some(next.unwrap_or(if right != line.rtl { index + 1 } else { index.saturating_sub(1) }))
|
||||
}
|
||||
|
||||
/// Left edge of a left-to-right line's first char (its end when it has none).
|
||||
fn line_start_x(&self, line: &Line) -> f64 {
|
||||
let gl = self.glyphs.get(line.glyph_start..line.glyph_end).unwrap_or(&[]);
|
||||
gl.iter().find(|g| g.char_end > g.char_start).map_or_else(|| self.line_end_x(line), |g| g.x)
|
||||
}
|
||||
|
||||
/// Where a line's text ends: its right edge, or its left edge for a right-to-left paragraph.
|
||||
fn line_end_x(&self, line: &Line) -> f64 {
|
||||
let glyphs = self.glyphs.get(line.glyph_start..line.glyph_end).unwrap_or(&[]).iter().filter(|g| g.char_end > g.char_start);
|
||||
@@ -1251,7 +1257,16 @@ impl TextLayout {
|
||||
let (a, b) = (start.min(end), start.max(end));
|
||||
let mut out = Vec::new();
|
||||
for (li, line) in self.lines.iter().enumerate() {
|
||||
if b <= line.char_start || a > line.char_end || (a == line.char_end && li + 1 < self.lines.len()) {
|
||||
let last = li + 1 == self.lines.len();
|
||||
// The empty line after a trailing break has no chars, but a selection reaching the
|
||||
// end of the text covers it: it shows a paragraph-break mark at the line's start.
|
||||
let trailing_empty = last && a < b && b >= line.char_start && line.char_start == line.char_end;
|
||||
if (b <= line.char_start && !trailing_empty) || a > line.char_end || (a == line.char_end && !last) {
|
||||
continue;
|
||||
}
|
||||
if trailing_empty {
|
||||
let x = self.line_end_x(line);
|
||||
out.push(Rect::new(x, line.top, x + 4.0, line.top + line.height));
|
||||
continue;
|
||||
}
|
||||
let gl = self.glyphs.get(line.glyph_start..line.glyph_end).unwrap_or(&[]);
|
||||
@@ -1259,10 +1274,12 @@ impl TextLayout {
|
||||
out.extend(Self::mixed_selection(gl, a, b, line));
|
||||
continue;
|
||||
}
|
||||
let (x0, _) = self.caret(a.max(line.char_start));
|
||||
// From the line's own start: `caret(char_start)` after a hard break is the end of the
|
||||
// line above.
|
||||
let x0 = if a <= line.char_start { self.line_start_x(line) } else { self.caret(a).0 };
|
||||
let x1 = if b >= line.char_end {
|
||||
// The last line has no paragraph break to show, so it ends at its glyphs.
|
||||
self.line_end_x(line) + if line.para_end && li + 1 < self.lines.len() && b > line.char_end.saturating_sub(1) { 4.0 } else { 0.0 }
|
||||
self.line_end_x(line) + if line.para_end && !last && b > line.char_end.saturating_sub(1) { 4.0 } else { 0.0 }
|
||||
} else {
|
||||
self.caret(b).0
|
||||
};
|
||||
|
||||
@@ -437,3 +437,47 @@ fn selection_through_text_end_stays_inside_auto_width_box() {
|
||||
let first = r.first().map_or(f64::NAN, |r| r.x1);
|
||||
assert!(first > l.caret(5).0 + 1.0, "{first}");
|
||||
}
|
||||
|
||||
/// The selection rect drawn on line `li` of `l`, if any.
|
||||
fn rect_on(l: &TextLayout, rects: &[figcraft_geom::Rect], li: usize) -> Option<figcraft_geom::Rect> {
|
||||
let top = l.lines.get(li)?.top;
|
||||
rects.iter().find(|r| (r.y0 - top).abs() < 1e-6).copied()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_highlights_an_empty_last_line() {
|
||||
if !have_fonts() {
|
||||
return;
|
||||
}
|
||||
// "Hello\n": the trailing break leaves an empty second line (#665).
|
||||
let l = layout(&TextData::new("Hello\n"), None, None);
|
||||
assert_eq!(l.lines.len(), 2);
|
||||
let Some(r) = rect_on(&l, &l.selection_rects(0, 6), 1) else { panic!("no highlight on the empty last line") };
|
||||
// A paragraph-break mark at the line's start, not at the end of "Hello" above.
|
||||
assert!(r.x0.abs() < 1e-6 && (r.width() - 4.0).abs() < 1e-6, "{r:?}");
|
||||
assert!(rect_on(&l, &l.selection_rects(5, 6), 1).is_some());
|
||||
// Selecting part of the first line, or a caret, leaves the empty line alone.
|
||||
assert!(l.selection_rects(0, 1).iter().all(|r| r.y0 < l.lines[1].top - 1e-6), "{:?}", l.selection_rects(0, 1));
|
||||
assert!(l.selection_rects(6, 6).is_empty());
|
||||
// A right-to-left paragraph marks its empty last line too.
|
||||
let rtl = layout(&TextData::new("\u{5E9}\u{5DC}\u{5D5}\u{5DD}\n"), None, None);
|
||||
let Some(r) = rect_on(&rtl, &rtl.selection_rects(0, 5), 1) else { panic!("no highlight on the empty right-to-left line") };
|
||||
assert!(r.x0.abs() < 1e-6 && (r.width() - 4.0).abs() < 1e-6, "{r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_after_a_line_break_starts_at_the_line_start() {
|
||||
if !have_fonts() {
|
||||
return;
|
||||
}
|
||||
// Not at the end of the line above, where `caret` puts the index after the break.
|
||||
let l = layout(&TextData::new("Hello\nworld"), None, None);
|
||||
let Some(r) = rect_on(&l, &l.selection_rects(0, 11), 1) else { panic!("no highlight on the second line") };
|
||||
assert!(r.x0.abs() < 1e-6 && r.x1 > l.caret(10).0, "{r:?}");
|
||||
// An empty line in the middle shows its break mark at its start.
|
||||
let m = layout(&TextData::new("Hello\n\nworld"), None, None);
|
||||
let Some(r) = rect_on(&m, &m.selection_rects(0, 12), 1) else { panic!("no highlight on the empty line") };
|
||||
assert!(r.x0.abs() < 1e-6 && (r.width() - 4.0).abs() < 1e-6, "{r:?}");
|
||||
// A break followed by text still doesn't highlight the next line's start.
|
||||
assert!(rect_on(&l, &l.selection_rects(0, 6), 1).is_none());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user