mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #760 from albayazid: Text: anchorCtr centres the drawn text block - right-aligned text stayed glued to the box edge (#736)
This commit is contained in:
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-10-15 | PDF export and rendering: `anchorCtr` centres the drawn text block — right-aligned text no longer ran past the box edge (#736) |
|
||||
| 2026-10-13 | Eyedropper: Shape Fill, Shape Outline, Font Color and every other colour menu end with an Eyedropper that takes the colour drawn at a point of the slide (hover shows it, a click applies it, Esc cancels); `slide.colorAt` gives the same from scripts and MCP (#367) |
|
||||
| 2026-10-13 | Slide show: Honeycomb's hexagons cover the whole slide again — the bottom quarter stayed black until the endpoint (#420) |
|
||||
| 2026-10-13 | Copy and Cut from the ribbon, the slide's right-click menu and the menus put the text on the system clipboard, as Ctrl+C and Ctrl+X do (#431) |
|
||||
|
||||
+14
-3
@@ -1911,9 +1911,20 @@ fn layout_scaled(ctx: &Ctx, shape: &Shape, body: &TextBody, bp: &BodyProps, opts
|
||||
// PowerPoint draws justified and distributed anchoring at the bottom.
|
||||
Anchor::Bottom | Anchor::Justified | Anchor::Distributed => inner.height() - used_h,
|
||||
};
|
||||
// anchorCtr: centre the block horizontally.
|
||||
let dx = if bp.anchor_ctr.unwrap_or(false) { ((inner.width() - out.content_width) / 2.0).max(0.0) } else { 0.0 };
|
||||
let ox = inner.x0 + dx;
|
||||
// anchorCtr: centre the drawn text block on the box. Lines sit where their alignment put
|
||||
// them, so the origin is set so the block's centre lands on the box's centre — a left-edge
|
||||
// offset alone pushed right-aligned lines out of the box (#736). Empty lines carry no ink
|
||||
// and stay out of the block. Overflowing content centres symmetrically past both edges.
|
||||
// Not applied to vertical text (there the offset would run along the wrong axis) or
|
||||
// multi-column bodies (centring unimplemented for them).
|
||||
let centre_block = bp.anchor_ctr.unwrap_or(false) && ncols == 1 && matches!(vert, TextDir::Horizontal);
|
||||
let ox = if centre_block && !all_lines.is_empty() {
|
||||
let ink = all_lines.iter().filter(|(li, ..)| li.end > li.start);
|
||||
let (bmin, bmax) = ink.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), (li, ..)| (a.min(li.extent.0), b.max(li.extent.1)));
|
||||
if bmax >= bmin { inner.x0 + inner.width() / 2.0 - (bmin + bmax) / 2.0 } else { inner.x0 }
|
||||
} else {
|
||||
inner.x0
|
||||
};
|
||||
let oy = inner.y0 + dy;
|
||||
// Emit runs, in visual order.
|
||||
for (line, place, _, bullet) in all_lines.iter_mut() {
|
||||
|
||||
@@ -1342,3 +1342,68 @@ fn stacked_combining_marks_take_a_face_that_draws_them() {
|
||||
}
|
||||
let _ = f;
|
||||
}
|
||||
|
||||
/// #736: `anchorCtr` centres the text block horizontally, whatever the paragraphs' own
|
||||
/// alignment, and overflowing content centres symmetrically past both edges.
|
||||
#[test]
|
||||
fn anchorctr_centres_the_text_block() {
|
||||
let text = "The shape is top - center - overriding paragraph right alignment.";
|
||||
// The block's centre: (min left + max right) over the laid-out lines, in box coordinates.
|
||||
let block_centre = |l: &TextLayout| -> f64 {
|
||||
// Empty lines carry no ink and sit where their alignment left them: not part of the block.
|
||||
let ink = l.lines.iter().filter(|li| li.end > li.start);
|
||||
let (min, max) = ink.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), li| (a.min(li.extent.0), b.max(li.extent.1)));
|
||||
(min + max) / 2.0
|
||||
};
|
||||
let para = |align: Option<Align>, text: &str| Paragraph {
|
||||
props: ParaProps { align, ..Default::default() },
|
||||
runs: vec![Run::new(text)],
|
||||
..Default::default()
|
||||
};
|
||||
let anchored =
|
||||
|paragraphs: Vec<Paragraph>| TextBody { body: BodyProps { anchor_ctr: Some(true), ..Default::default() }, paragraphs, ..Default::default() };
|
||||
// One right-aligned line in a wide box: the block is centred, not pushed past the right edge.
|
||||
let body_right = anchored(vec![para(Some(Align::Right), text)]);
|
||||
let (p, mut sh) = setup("", 828.0, 104.0);
|
||||
sh.text = Some(body_right);
|
||||
let l = lay(&p, &sh, 828.0, 104.0);
|
||||
assert!((block_centre(&l) - 414.0).abs() <= 1.0, "right-aligned: centre {}, want 414", block_centre(&l));
|
||||
// Left-aligned text centres the same way.
|
||||
let body_left = anchored(vec![para(None, text)]);
|
||||
let (p, mut sh) = setup("", 828.0, 104.0);
|
||||
sh.text = Some(body_left);
|
||||
let l = lay(&p, &sh, 828.0, 104.0);
|
||||
assert!((block_centre(&l) - 414.0).abs() <= 1.0, "left-aligned: centre {}, want 414", block_centre(&l));
|
||||
// A trailing empty paragraph carries no ink and must not skew the block.
|
||||
let body_empty = anchored(vec![para(Some(Align::Right), text), para(None, "")]);
|
||||
let (p, mut sh) = setup("", 828.0, 104.0);
|
||||
sh.text = Some(body_empty);
|
||||
let l = lay(&p, &sh, 828.0, 104.0);
|
||||
assert!((block_centre(&l) - 414.0).abs() <= 1.0, "trailing empty paragraph: centre {}, want 414", block_centre(&l));
|
||||
// Mixed alignments: the drawn block (not either alignment edge) is what centres.
|
||||
let body_mixed = anchored(vec![para(Some(Align::Right), text), para(None, "short")]);
|
||||
let (p, mut sh) = setup("", 828.0, 104.0);
|
||||
sh.text = Some(body_mixed);
|
||||
let l = lay(&p, &sh, 828.0, 104.0);
|
||||
assert!((block_centre(&l) - 414.0).abs() <= 1.0, "mixed: centre {}, want 414", block_centre(&l));
|
||||
// Content wider than the box overflows symmetrically past both edges (no wrap).
|
||||
let body_wide = TextBody {
|
||||
body: BodyProps { anchor_ctr: Some(true), wrap: Some(false), ..Default::default() },
|
||||
paragraphs: vec![para(Some(Align::Right), text)],
|
||||
..Default::default()
|
||||
};
|
||||
let (p, mut sh) = setup("", 300.0, 104.0);
|
||||
sh.text = Some(body_wide);
|
||||
let l = lay(&p, &sh, 300.0, 104.0);
|
||||
assert!((block_centre(&l) - 150.0).abs() <= 2.0, "overflowing: centre {}, want 150", block_centre(&l));
|
||||
// Control: without anchorCtr a right-aligned line still ends at the box's right edge.
|
||||
let body_control = TextBody { body: BodyProps::default(), paragraphs: vec![para(Some(Align::Right), text)], ..Default::default() };
|
||||
let (p, mut sh) = setup(text, 828.0, 104.0);
|
||||
sh.text = Some(body_control);
|
||||
let l = lay(&p, &sh, 828.0, 104.0);
|
||||
assert!(
|
||||
l.lines.iter().all(|li| li.extent.1 > 810.0),
|
||||
"control: right-aligned at the box edge, got {:?}",
|
||||
l.lines.iter().map(|li| li.extent).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user