Merge pull request #50 from LloydNicholson/rotated-wrap-text
Issue #48: rotated text with Wrap Text draws upright (grid and print) # Conflicts: # crates/ui-egui/tests/interaction.rs
This commit is contained in:
commit
f736286f24
|
|
@ -923,7 +923,15 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
|
|||
HAlign::Justify | HAlign::Fill => HAlign::Left,
|
||||
other => other,
|
||||
};
|
||||
let lines: Vec<String> = if wrap { wrap_lines(&text, font, size, avail.max(size)) } else { vec![text] };
|
||||
let rot = st.align.rotation;
|
||||
let rotated = rot != 0 && rot != 255;
|
||||
// Rotated text wraps along its own direction: upright (±90°) text to the row height.
|
||||
let lines: Vec<String> = match (wrap, rotated) {
|
||||
(true, false) => wrap_lines(&text, font, size, avail.max(size)),
|
||||
(true, true) if rot.abs() == 90 => wrap_lines(&text, font, size, (h - 2.0 * pad).max(size)),
|
||||
(true, true) => vec![text.lines().next().unwrap_or_default().to_string()],
|
||||
(false, _) => vec![text],
|
||||
};
|
||||
let line_h = size * 1.2;
|
||||
let n = lines.len().max(1) as f32;
|
||||
let first_baseline = match st.align.v {
|
||||
|
|
@ -933,7 +941,7 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
|
|||
};
|
||||
// Overflow into empty neighbours (single-line text, not merged).
|
||||
let (mut clip_l, mut clip_r) = (x, x + w);
|
||||
if !wrap && !merged && !is_num && tw + 2.0 * pad + indent > w {
|
||||
if !wrap && !merged && !is_num && rot.abs() != 90 && tw + 2.0 * pad + indent > w {
|
||||
let need = tw + 2.0 * pad + indent - w;
|
||||
let grow = |dir: i64, mut need: f32| -> f32 {
|
||||
let mut ext = 0.0;
|
||||
|
|
@ -966,13 +974,20 @@ fn draw_text(pg: &mut Page, b: &Block, c: CellRef, l: &Look, rect: (f32, f32, f3
|
|||
let clip_r = clip_r.min(limits.1);
|
||||
pg.save();
|
||||
pg.clip_rect(clip_l, y, (clip_r - clip_l).max(0.0), h);
|
||||
let rot = st.align.rotation;
|
||||
if rot != 0 && rot != 255 && !wrap {
|
||||
if rotated {
|
||||
// Rotated text: anchored at the bottom-left (upward) or top-left (downward) of the cell.
|
||||
// Wrapped lines stack rightward for upward text and leftward for downward text, so
|
||||
// downward text starts its first line far enough in for the last to end at the left pad.
|
||||
let deg = (rot as f32).clamp(-90.0, 90.0);
|
||||
let angle = -deg.to_radians();
|
||||
let (ox, oy) = if deg > 0.0 { (x + pad + size * 0.8 * deg.to_radians().sin(), y + h - pad) } else { (x + pad, y + pad + size * 0.8) };
|
||||
if let Some(t) = lines.first() {
|
||||
let last = lines.len().saturating_sub(1) as f32;
|
||||
for (i, t) in lines.iter().enumerate() {
|
||||
let i = i as f32;
|
||||
let (ox, oy) = if deg > 0.0 {
|
||||
(x + pad + size * 0.8 * deg.to_radians().sin() + i * line_h, y + h - pad)
|
||||
} else {
|
||||
(x + pad + (last - i) * line_h, y + pad + size * 0.8)
|
||||
};
|
||||
pg.text_rotated(ox, oy, size, font, color, t, angle);
|
||||
}
|
||||
pg.restore();
|
||||
|
|
@ -1762,4 +1777,26 @@ mod tests {
|
|||
let w = wrap_lines("Supercalifragilistic", Font::Helvetica, 10.0, 20.0);
|
||||
assert!(w.len() > 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_wrapped_text_prints_rotated() {
|
||||
// Upright headers over narrow columns (mark sheets) combine rotation with Wrap Text.
|
||||
let mut s = s();
|
||||
let header = "Term 1 : Drama : Continuous Assessment";
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[header, header]]})).unwrap();
|
||||
s.execute("home.wrapText", json!({"range": "A1:B1", "on": true})).unwrap();
|
||||
s.execute("home.orientation", json!({"range": "A1", "angle": "up"})).unwrap();
|
||||
s.execute("home.orientation", json!({"range": "B1", "angle": "down"})).unwrap();
|
||||
s.execute("home.columnWidth", json!({"cols": "A:B", "chars": 6})).unwrap();
|
||||
s.execute("home.rowHeight", json!({"rows": "1:1", "height": 267})).unwrap();
|
||||
let (pdf, _) = pdf_of(&mut s, json!({}));
|
||||
let pdf = String::from_utf8_lossy(&pdf);
|
||||
// The page holds only these two cells, so every text block on it is a piece of a header.
|
||||
let blocks: Vec<&str> = pdf.split("BT\n").skip(1).filter(|b| b.contains(" Tj")).collect();
|
||||
let up = blocks.iter().filter(|b| b.contains("\n0 1 -1 0 ")).count();
|
||||
let down = blocks.iter().filter(|b| b.contains("\n0 -1 1 0 ")).count();
|
||||
// Every piece of both headers is rotated, each wrapped along the row height into a few long lines.
|
||||
assert_eq!(up + down, blocks.len(), "a header line printed flat: {blocks:?}");
|
||||
assert!((1..=3).contains(&up) && (1..=3).contains(&down), "up {up}, down {down}: {blocks:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -567,7 +567,9 @@ fn paint_quadrant(p: &Painter, geo: &Geo, wb: &Workbook, si: usize, sh: &Sheet,
|
|||
};
|
||||
let indent = st.align.indent as f32 * 9.0 * z;
|
||||
let pad = 2.5 * z;
|
||||
if st.align.wrap || st.align.h == HAlign::Justify || text.contains('\n') && st.align.wrap {
|
||||
let rotated = st.align.rotation != 0 && st.align.rotation != 255;
|
||||
// Rotated text wraps along its own direction (below), not across the column width.
|
||||
if !rotated && (st.align.wrap || st.align.h == HAlign::Justify) {
|
||||
let wrap_w = (rect.width() - 2.0 * pad - indent).max(4.0);
|
||||
let mut job = egui::text::LayoutJob::simple(text.clone(), font.clone(), color, wrap_w);
|
||||
job.halign = match halign {
|
||||
|
|
@ -612,9 +614,9 @@ fn paint_quadrant(p: &Painter, geo: &Geo, wb: &Workbook, si: usize, sh: &Sheet,
|
|||
galley = p.layout_no_wrap(s, font.clone(), color);
|
||||
w = galley.size().x;
|
||||
}
|
||||
// Overflow into empty neighbours for text.
|
||||
// Overflow into empty neighbours for text (not upright text, whose width is its line height).
|
||||
let mut clip = *rect;
|
||||
if !numeric && !matches!(v, Value::Number(_)) && w > rect.width() - 2.0 * pad && in_merge(c).is_none() {
|
||||
if !numeric && !matches!(v, Value::Number(_)) && st.align.rotation.abs() != 90 && w > rect.width() - 2.0 * pad && in_merge(c).is_none() {
|
||||
let mut right = rect.right();
|
||||
let mut left = rect.left();
|
||||
if matches!(halign, HAlign::Left | HAlign::Center | HAlign::Justify) {
|
||||
|
|
@ -657,9 +659,23 @@ fn paint_quadrant(p: &Painter, geo: &Geo, wb: &Workbook, si: usize, sh: &Sheet,
|
|||
VAlign::Bottom => rect.bottom() - gh - 0.5 * z,
|
||||
};
|
||||
let cp = p.with_clip_rect(clip.intersect(p.clip_rect()));
|
||||
if st.align.rotation != 0 && st.align.rotation != 255 {
|
||||
if rotated {
|
||||
let angle = -(st.align.rotation as f32).to_radians();
|
||||
let mut shape = egui::epaint::TextShape::new(pos2(rect.left() + pad, rect.bottom() - pad), galley.clone(), color);
|
||||
// Upright (±90°) wrapped text wraps to the row height: lines then stack across the column.
|
||||
let galley = if st.align.wrap && st.align.rotation.abs() == 90 {
|
||||
let job = egui::text::LayoutJob::simple(text.clone(), font.clone(), color, (rect.height() - 2.0 * pad).max(4.0));
|
||||
p.layout_job(job)
|
||||
} else {
|
||||
galley.clone()
|
||||
};
|
||||
// Upward text grows up and right from the bottom-left corner; downward text grows down
|
||||
// and left from its anchor, so that anchor sits one block-height in from the top-left.
|
||||
let pos = if angle > 0.0 {
|
||||
pos2(rect.left() + pad + galley.size().y * angle.sin(), rect.top() + pad)
|
||||
} else {
|
||||
pos2(rect.left() + pad, rect.bottom() - pad)
|
||||
};
|
||||
let mut shape = egui::epaint::TextShape::new(pos, galley, color);
|
||||
shape.angle = angle;
|
||||
cp.add(shape);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -222,3 +222,42 @@ fn general_numbers_fit_by_the_cells_own_font() {
|
|||
// A number that really doesn't fit still shows #### (once: only B1).
|
||||
assert_eq!(texts.iter().filter(|t| t.starts_with('#')).count(), 1, "{texts:?}");
|
||||
}
|
||||
|
||||
fn text_shapes(shape: &egui::Shape, clip: egui::Rect, out: &mut Vec<(egui::epaint::TextShape, egui::Rect)>) {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => out.push((t.clone(), clip)),
|
||||
egui::Shape::Vec(v) => v.iter().for_each(|s| text_shapes(s, clip, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotated_wrapped_text_is_drawn_rotated_inside_its_cell() {
|
||||
// Narrow columns with tall, upright headers (mark sheets): rotation and Wrap Text together.
|
||||
let header = "Term 1 : Drama : Continuous Assessment";
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[header, header]]})).unwrap();
|
||||
s.execute("home.wrapText", json!({"range": "A1:B1", "on": true})).unwrap();
|
||||
s.execute("home.orientation", json!({"range": "A1", "angle": "up"})).unwrap();
|
||||
s.execute("home.orientation", json!({"range": "B1", "angle": "down"})).unwrap();
|
||||
s.execute("home.columnWidth", json!({"cols": "A:B", "chars": 6})).unwrap();
|
||||
s.execute("home.rowHeight", json!({"rows": "1:1", "height": 267})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "C3"})).unwrap();
|
||||
let h = harness(s);
|
||||
let mut found = Vec::new();
|
||||
for c in &h.output().shapes {
|
||||
text_shapes(&c.shape, c.clip_rect, &mut found);
|
||||
}
|
||||
let rotated: Vec<_> = found.iter().filter(|(t, _)| t.galley.job.text == header).collect();
|
||||
assert_eq!(rotated.len(), 2, "both headers drawn as one text block each");
|
||||
for (t, clip) in rotated {
|
||||
assert!((t.angle.abs() - std::f32::consts::FRAC_PI_2).abs() < 1e-3, "drawn at ±90°, got {}", t.angle);
|
||||
// Wrapped along the row height into a couple of long lines, not across the narrow column.
|
||||
assert!(t.galley.rows.len() <= 3, "{} lines", t.galley.rows.len());
|
||||
let bb = t.visual_bounding_rect();
|
||||
assert!(bb.height() > bb.width(), "upright, not flat: {bb:?}");
|
||||
assert!(clip.expand(1.0).contains_rect(bb), "inside its cell: {bb:?} not in {clip:?}");
|
||||
// Upright text never spills into the neighbouring columns: the clip is the cell itself.
|
||||
assert!(clip.width() < 70.0, "clip spans one 6-character column, got {clip:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue