mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Merge branch 'feat/vj-baseline-grid' of github.com:dotCooCoo/designcraft into codex/integrate-2658b
This commit is contained in:
2 files changed
+110
-5
No files matched your search
@@ -1858,8 +1858,14 @@ fn position_line(input: PositionLineInput<'_, '_>) -> LinePosition {
|
||||
if cur.last_baseline.is_some() {
|
||||
baseline += cur.last_reference - reference;
|
||||
}
|
||||
let grid = f.grid.filter(|&(_, inc)| {
|
||||
inc > 0.0 && inc.is_finite() && (pp.grid_align == GridAlign::AllLines || (pp.grid_align == GridAlign::FirstLineOnly && line_no == 0))
|
||||
// A step so small that the column holds more steps than an f64 can count is no grid: the
|
||||
// line is placed as it is without one.
|
||||
let countable = |g_start: f64, inc: f64| [baseline, col.y0, col.y1].iter().all(|y| ((y - g_start) / inc).is_finite());
|
||||
let grid = f.grid.filter(|&(g_start, inc)| {
|
||||
inc > 0.0
|
||||
&& inc.is_finite()
|
||||
&& countable(g_start, inc)
|
||||
&& (pp.grid_align == GridAlign::AllLines || (pp.grid_align == GridAlign::FirstLineOnly && line_no == 0))
|
||||
});
|
||||
if let Some((g_start, inc)) = grid {
|
||||
let n = pp.grid_gyoudori.min(grid::MAX_GYOUDORI);
|
||||
@@ -6151,9 +6157,26 @@ fn vertical_justify(
|
||||
// The last baseline stays above the bottom of the text, and the rows above it fit.
|
||||
let most = (space / inc + 1e-9).floor().min((room / unit + 1e-9).floor()).max(0.0);
|
||||
// Bottom and Justify take the lowest grid line, Center the one nearest the middle.
|
||||
let mut steps = if center { (space / 2.0 / inc).round().min(most) } else { most };
|
||||
if steps > 0.0 && !fits(&shifts(steps * unit)) {
|
||||
steps -= 1.0;
|
||||
let target = if center { (space / 2.0 / inc).round().min(most) } else { most };
|
||||
// A shaped frame can narrow and widen again, so the target step may not fit: take
|
||||
// the fitting step nearest it, up to `most` (the lower one on a tie, and no step
|
||||
// when that is as near), and leave the lines in place when none within
|
||||
// `MAX_GRID_TRIES` steps of it fits.
|
||||
const MAX_GRID_TRIES: usize = 512;
|
||||
let mut steps = 0.0;
|
||||
for d in 0..MAX_GRID_TRIES {
|
||||
let (below, above) = (target - d as f64, target + d as f64);
|
||||
if below <= 0.0 {
|
||||
break;
|
||||
}
|
||||
if fits(&shifts(below * unit)) {
|
||||
steps = below;
|
||||
break;
|
||||
}
|
||||
if d > 0 && above <= most && fits(&shifts(above * unit)) {
|
||||
steps = above;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// With no step taken the lines stay put, even when Center's doubled step overflows.
|
||||
shifts(if steps > 0.0 { steps * unit } else { 0.0 })
|
||||
|
||||
@@ -2799,6 +2799,25 @@ fn vertical_justification_keeps_grid_aligned_lines_on_the_grid() {
|
||||
let (_, first_only, _) = compose_with(Bottom, GridAlign::FirstLineOnly);
|
||||
assert!(first_only.iter().all(|&b| on_grid(b, grid)), "Bottom, first lines only: {first_only:?}");
|
||||
|
||||
// Justify with a paragraph that wraps: with all lines aligned the last line lands on the grid;
|
||||
// with first lines only it is not a first line, so it goes down to the bottom inset.
|
||||
let wrapped = "The first line of this paragraph sits on the baseline grid, and the lines after it keep the leading they have.";
|
||||
let justify_wrapped = |align: GridAlign| {
|
||||
let (lines, _) = grid_doc(wrapped, ParaAttrs { grid_align: Some(align), ..Default::default() }, |d, sid| {
|
||||
let spec = frame_specs(d, sid).remove(0);
|
||||
d.item_mut(spec.id).unwrap().text_frame_mut().unwrap().options.vertical_justification = Justify;
|
||||
});
|
||||
lines.iter().map(|l| l.baseline).collect::<Vec<_>>()
|
||||
};
|
||||
let all = justify_wrapped(GridAlign::AllLines);
|
||||
assert!(all.len() >= 2, "the paragraph wraps: {all:?}");
|
||||
let last = all[all.len() - 1];
|
||||
assert!(on_grid(last, grid) && last <= area.y1 + 1e-6 && last > area.y1 - inc, "Justify, all lines: last baseline {last}");
|
||||
let first_lines = justify_wrapped(GridAlign::FirstLineOnly);
|
||||
assert!(first_lines.len() >= 2, "the paragraph wraps: {first_lines:?}");
|
||||
let last = first_lines[first_lines.len() - 1];
|
||||
assert!((last - area.y1).abs() < 1e-6 && !on_grid(last, grid), "Justify, first lines only: last baseline {last}, inset at {}", area.y1);
|
||||
|
||||
// Without grid alignment the last baseline goes down to the bottom inset.
|
||||
let (_, free, _) = compose_with(Bottom, GridAlign::None);
|
||||
assert!((free[2] - area.y1).abs() < 1e-6, "Bottom without the grid: {free:?}");
|
||||
@@ -2839,6 +2858,69 @@ fn vertical_justification_with_an_extreme_grid_increment_stays_finite() {
|
||||
// A step far larger than the room: the line does not move.
|
||||
let (_, b) = compose_with(Center, 1e308);
|
||||
assert!((b - natural).abs() < 1e-6, "Center with a huge increment: baseline {b}, natural {natural}");
|
||||
|
||||
// A grid that starts away from the line, with a step too small to count the distance: the line
|
||||
// is placed as it is without a grid.
|
||||
for mode in [Top, Bottom, Center, Justify] {
|
||||
let mut area = Rect::ZERO;
|
||||
let (lines, _) = grid_doc("One", ParaAttrs { grid_align: Some(GridAlign::AllLines), ..Default::default() }, |d, sid| {
|
||||
let spec = frame_specs(d, sid).remove(0);
|
||||
area = spec.area;
|
||||
let opts = &mut d.item_mut(spec.id).unwrap().text_frame_mut().unwrap().options;
|
||||
opts.baseline_grid = Some(designcraft_doc::FrameBaselineGrid::new(start - 5.0, 1e-310));
|
||||
opts.vertical_justification = mode;
|
||||
});
|
||||
let b = lines[0].baseline;
|
||||
assert!(b.is_finite() && b >= natural - 1e-6 && b <= area.y1 + 1e-6, "{mode:?}, grid away from the line: baseline {b}, text area {area:?}");
|
||||
if mode == Top {
|
||||
assert!((b - natural).abs() < 1e-6, "Top, grid away from the line: baseline {b}, natural {natural}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In a shaped frame that narrows and widens again, Align to Baseline Grid takes the grid step
|
||||
/// nearest the place Bottom or Center would choose at which the line's slot fits the outline:
|
||||
/// Bottom goes up past steps in the narrow part, and Center goes down a step when the step nearest
|
||||
/// the middle is in the narrow part and the next one fits.
|
||||
#[test]
|
||||
fn vertical_justification_on_the_grid_skips_steps_that_leave_a_shaped_frame() {
|
||||
use designcraft_doc::GridAlign;
|
||||
use designcraft_doc::VerticalJustification::{self, Bottom, Center, Top};
|
||||
use designcraft_geom::{Anchor, PathData, Point, SubPath};
|
||||
// Full width down to 100 pt, a waist at 150 pt, full width again from 200 to 230 pt, then a
|
||||
// point at 300 pt.
|
||||
let pts = [(0.0, 0.0), (300.0, 0.0), (300.0, 100.0), (200.0, 150.0), (300.0, 200.0), (300.0, 230.0), (150.0, 300.0)];
|
||||
let pts: Vec<(f64, f64)> = pts.iter().copied().chain([(0.0, 230.0), (0.0, 200.0), (100.0, 150.0), (0.0, 100.0)]).collect();
|
||||
let compose_with = |mode: VerticalJustification, inc: f64| {
|
||||
let para = ParaAttrs { grid_align: Some(GridAlign::AllLines), ..Default::default() };
|
||||
let (mut d, sid, fid) = doc_with("Typography", Rect::new(0.0, 0.0, 300.0, 300.0), para);
|
||||
let it = d.item_mut(fid).unwrap();
|
||||
it.path = PathData::single(SubPath { anchors: pts.iter().map(|&(x, y)| Anchor::corner(Point::new(x, y))).collect(), closed: true });
|
||||
let o = &mut it.text_frame_mut().unwrap().options;
|
||||
o.inset = [0.0; 4];
|
||||
o.baseline_grid = Some(designcraft_doc::FrameBaselineGrid::new(0.0, inc));
|
||||
o.vertical_justification = mode;
|
||||
let grid = frame_specs(&d, sid)[0].grid.unwrap();
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let l = all_lines(&cs)[0];
|
||||
(grid, l.baseline, l.baseline - l.ascent)
|
||||
};
|
||||
let lower = |b: f64, band_top: f64| band_top >= 200.0 - 1e-6 && b <= 230.0 + 1e-6;
|
||||
|
||||
// A 40 pt grid: the lowest room ends between 160 and 200 pt, and the steps above it down to
|
||||
// 120 pt put the line in the waist.
|
||||
let (grid, top, _) = compose_with(Top, 40.0);
|
||||
assert!(on_grid(top, grid), "Top: baseline {top} on the grid {grid:?}");
|
||||
let (_, b, band_top) = compose_with(Bottom, 40.0);
|
||||
assert!(on_grid(b, grid), "Bottom: baseline {b} on the grid {grid:?}");
|
||||
assert!(b <= 100.0 + 1e-6 || lower(b, band_top), "Bottom: the line's band {band_top}..{b} reaches into the narrow part");
|
||||
assert!(b > top, "Bottom moves the line down a grid step that fits: {b}, Top at {top}");
|
||||
|
||||
// A 45 pt grid: the step nearest the middle puts the baseline at 180 pt, in the waist; the
|
||||
// step below it fits in the lower full-width part.
|
||||
let (grid, b, band_top) = compose_with(Center, 45.0);
|
||||
assert!(on_grid(b, grid), "Center: baseline {b} on the grid {grid:?}");
|
||||
assert!(lower(b, band_top), "Center: the line's band {band_top}..{b} is not in the lower full-width part");
|
||||
}
|
||||
|
||||
/// A corner radius leaves a Cap Height first line where it sits in a rectangular frame, with Top
|
||||
|
||||
Reference in new issue
Block a user