mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 11:16:50 -06:00
Merge #84 into fix/29-kerning-across-runs
This commit is contained in:
+30
-12
@@ -704,23 +704,27 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
let drop_cap = split_drop_cap(db, &mut sp.glyphs, drop_end, pp.drop_cap_lines, base_leading, base_cap, drop_grid, drop_align);
|
||||
// List labels take the default super/subscript settings.
|
||||
let label_env = shape::TypeEnv { adv: Default::default(), ..env };
|
||||
match pp.list_type {
|
||||
let list_label = match pp.list_type {
|
||||
designcraft_doc::ListType::Numbers if !pp.list_name.is_empty() => {
|
||||
// A named list: carries on past other paragraphs (and from earlier stories).
|
||||
let n = named_numbers.get(pi).copied().flatten().unwrap_or(1);
|
||||
let label = format!("{}.{}", pp.number_style.format(n), pp.list_separator);
|
||||
prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, label_env, &mut table);
|
||||
Some(format!("{}.{}", pp.number_style.format(n), pp.list_separator))
|
||||
}
|
||||
designcraft_doc::ListType::Numbers => {
|
||||
list_counter = pp.start_at.map_or(list_counter + 1, |s| s.max(1));
|
||||
let label = format!("{}.{}", pp.number_style.format(list_counter), pp.list_separator);
|
||||
prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, label_env, &mut table);
|
||||
Some(format!("{}.{}", pp.number_style.format(list_counter), pp.list_separator))
|
||||
}
|
||||
designcraft_doc::ListType::Bullets => {
|
||||
let label = format!("{}{}", pp.bullet_char, pp.list_separator);
|
||||
prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, label_env, &mut table);
|
||||
designcraft_doc::ListType::Bullets => Some(format!("{}{}", pp.bullet_char, pp.list_separator)),
|
||||
designcraft_doc::ListType::None => {
|
||||
list_counter = 0;
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(label) = list_label {
|
||||
let n = prepend_label(db, &mut sp.glyphs, &label, prange.start, &base_chars, label_env, &mut table);
|
||||
for g in sp.glyphs.iter_mut().take(n) {
|
||||
g.list_label = true;
|
||||
}
|
||||
designcraft_doc::ListType::None => list_counter = 0,
|
||||
}
|
||||
if pi == 0
|
||||
&& let Some(label) = &opts.label
|
||||
@@ -1564,8 +1568,9 @@ fn line_metrics(
|
||||
base: &designcraft_doc::CharProps,
|
||||
) -> (f64, Tops, f64, f64) {
|
||||
let src: &[Glyph] = if line.is_empty() { all.get(s..(s + 1).min(all.len())).unwrap_or(&[]) } else { line };
|
||||
let face = || db.face(&base.font_family, &base.font_style);
|
||||
if src.is_empty() {
|
||||
let face = db.face(&base.font_family, &base.font_style);
|
||||
let face = face();
|
||||
let k = base_size / face.upem;
|
||||
let tops = Tops { typo_ascent: face.typo_ascent * k, cap: face.cap_height * k, xh: face.x_height * k };
|
||||
return (face.ascent * k, tops, face.descent * k, base_leading);
|
||||
@@ -1575,16 +1580,26 @@ fn line_metrics(
|
||||
let mut desc: f64 = 0.0;
|
||||
let mut lead: f64 = 0.0;
|
||||
let reference = src.iter().max_by(|a, b| a.size.total_cmp(&b.size));
|
||||
let mut own_text = false;
|
||||
for g in src {
|
||||
let shift = g.shift + reference.map_or(0.0, |r| cjk_alignment_shift(g, r));
|
||||
let up = shift.max(0.0);
|
||||
asc = asc.max(g.ascent + up);
|
||||
tops.typo_ascent = tops.typo_ascent.max(g.typo_ascent + up);
|
||||
// A list's bullet or number doesn't raise the Ascent offset: only the line's own text does.
|
||||
if !g.list_label {
|
||||
own_text = true;
|
||||
tops.typo_ascent = tops.typo_ascent.max(g.typo_ascent + up);
|
||||
}
|
||||
tops.cap = tops.cap.max(g.cap + up);
|
||||
tops.xh = tops.xh.max(g.xh + up);
|
||||
desc = desc.max(g.descent - shift.min(0.0));
|
||||
lead = lead.max(g.leading);
|
||||
}
|
||||
if !own_text {
|
||||
// Only a label (an empty list paragraph): as an empty paragraph.
|
||||
let face = face();
|
||||
tops.typo_ascent = face.typo_ascent * base_size / face.upem;
|
||||
}
|
||||
(asc, tops, desc, lead)
|
||||
}
|
||||
|
||||
@@ -2307,6 +2322,7 @@ fn place_drop_cap(dc: &DropCap, at: f64, rtl: bool) -> Vec<PlacedGlyph> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Put generated `label` text before the paragraph's glyphs; returns how many glyphs it added.
|
||||
fn prepend_label(
|
||||
db: &ScopedFonts<'_>,
|
||||
glyphs: &mut Vec<Glyph>,
|
||||
@@ -2315,7 +2331,7 @@ fn prepend_label(
|
||||
base: &designcraft_doc::CharProps,
|
||||
env: shape::TypeEnv,
|
||||
table: &mut StyleTable<'_>,
|
||||
) {
|
||||
) -> usize {
|
||||
// Shape the label as a tiny standalone story so it uses the paragraph's base character style.
|
||||
let mut tmp = Story::new(StoryId(0));
|
||||
tmp.insert(0, label);
|
||||
@@ -2330,8 +2346,10 @@ fn prepend_label(
|
||||
g
|
||||
})
|
||||
.collect();
|
||||
let n = pre.len();
|
||||
pre.append(glyphs);
|
||||
*glyphs = pre;
|
||||
n
|
||||
}
|
||||
|
||||
type LimitsKey = (usize, usize, usize, bool);
|
||||
|
||||
@@ -28,7 +28,8 @@ pub struct Glyph {
|
||||
/// Baseline shift (positive = up), including super/subscript.
|
||||
pub shift: f64,
|
||||
pub ascent: f64,
|
||||
/// The font's typographic ascender, scaled like `ascent`: the Ascent first baseline offset.
|
||||
/// The font's typographic ascender at the point size (vertical scale doesn't apply): the Ascent
|
||||
/// first baseline offset.
|
||||
pub typo_ascent: f64,
|
||||
pub descent: f64,
|
||||
/// The leading this character asks for (absolute, or auto = size × auto %).
|
||||
@@ -68,6 +69,9 @@ pub struct Glyph {
|
||||
/// Upright in a vertical frame: shaped top to bottom (`adv` is its vertical advance) and hung
|
||||
/// from its vertical origin on the line's centre (see [`crate::PlacedGlyph::vertical_xf`]).
|
||||
pub upright: bool,
|
||||
/// Part of the bullet or number the paragraph's list adds: not the line's own text, so it
|
||||
/// doesn't raise the Ascent first baseline.
|
||||
pub list_label: bool,
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
@@ -872,7 +876,7 @@ fn control_glyph(face: &Arc<FontFace>, p: &CharProps, auto_leading: TypeEnv, sty
|
||||
sy: k * p.v_scale,
|
||||
shift,
|
||||
ascent,
|
||||
typo_ascent: face.typo_ascent * k * p.v_scale,
|
||||
typo_ascent: face.typo_ascent * k,
|
||||
descent,
|
||||
leading,
|
||||
cap,
|
||||
@@ -898,6 +902,7 @@ fn control_glyph(face: &Arc<FontFace>, p: &CharProps, auto_leading: TypeEnv, sty
|
||||
shaping_rtl: false,
|
||||
lang: designcraft_doc::language_tag(&p.language),
|
||||
upright: false,
|
||||
list_label: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,7 +996,7 @@ fn shape_segment(
|
||||
sy: k * p.v_scale,
|
||||
shift,
|
||||
ascent,
|
||||
typo_ascent: face.typo_ascent * k * p.v_scale,
|
||||
typo_ascent: face.typo_ascent * k,
|
||||
descent,
|
||||
leading,
|
||||
cap,
|
||||
@@ -1021,6 +1026,7 @@ fn shape_segment(
|
||||
shaping_rtl: sg.rtl,
|
||||
lang,
|
||||
upright: up,
|
||||
list_label: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+122
-12
@@ -1395,18 +1395,6 @@ fn ascent_first_baseline_uses_the_typographic_ascender() {
|
||||
assert!(close(at(FirstBaseline::XHeight, 0.0), top + 0.486 * 20.0));
|
||||
assert!(close(at(FirstBaseline::Leading, 0.0), top + 24.0));
|
||||
assert!(close(at(FirstBaseline::Fixed, 0.0), top));
|
||||
// Vertical scale scales it like the ascent.
|
||||
let (mut d, sid, fid) = doc_with("Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
d.story_mut(sid).unwrap().format_chars(0..3, |f| {
|
||||
f.over.font_family = Some("TypoAscent 07".into());
|
||||
f.over.size = Some(20.0);
|
||||
f.over.v_scale = Some(1.5);
|
||||
});
|
||||
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.inset = [4.0, 0.0, 0.0, 0.0];
|
||||
let l = &compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0];
|
||||
assert!(close(l.baseline, top + 21.0), "{}", l.baseline);
|
||||
// The line box keeps the font's ascent.
|
||||
assert!(close(l.ascent, 30.0), "{}", l.ascent);
|
||||
// A mixed first line takes its tallest typographic ascender: 0.7 × 20 pt beats 1 em × 10 pt …
|
||||
let (mut d, sid, fid) = doc_with("Hxg Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
let st = d.story_mut(sid).unwrap();
|
||||
@@ -1451,6 +1439,102 @@ fn vertical_frames_keep_the_ascent_first_baseline() {
|
||||
assert!((b - 20.0).abs() < 0.01, "{b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_visible_frame_stroke_insets_the_text() {
|
||||
use designcraft_doc::{Stroke, StrokeAlign};
|
||||
let line = |stroke: Option<(StrokeAlign, &str)>| {
|
||||
let (mut d, sid, fid) = doc_with("Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
let it = d.item_mut(fid).unwrap();
|
||||
if let Some((align, swatch)) = stroke {
|
||||
it.stroke = Stroke { weight: 4.0, align, swatch: swatch.into(), ..Stroke::default() };
|
||||
}
|
||||
it.text_frame_mut().unwrap().options.inset = [4.0; 4];
|
||||
let l = &compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0];
|
||||
(l.baseline, l.x0, l.x1)
|
||||
};
|
||||
let black = designcraft_color::swatch::BLACK;
|
||||
let (b, x0, x1) = line(None);
|
||||
// The part of the 4 pt stroke inside the frame: half of it centred, all of it inside, none
|
||||
// outside or without a colour; on the left and right as on the top.
|
||||
for (stroke, d) in [
|
||||
((StrokeAlign::Center, black), 2.0),
|
||||
((StrokeAlign::Inside, black), 4.0),
|
||||
((StrokeAlign::Outside, black), 0.0),
|
||||
((StrokeAlign::Inside, designcraft_color::swatch::NONE), 0.0),
|
||||
] {
|
||||
let (sb, sx0, sx1) = line(Some(stroke));
|
||||
assert!((sb - (b + d)).abs() < 1e-6, "{stroke:?}: baseline {sb} vs {b}");
|
||||
assert!((sx0 - (x0 + d)).abs() < 1e-6 && (sx1 - (x1 - d)).abs() < 1e-6, "{stroke:?}: {sx0}..{sx1} vs {x0}..{x1}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_labels_do_not_raise_the_ascent_first_baseline() {
|
||||
use designcraft_doc::{CharAttrs, ListType};
|
||||
// The paragraph's own character size (the label's) is 40 pt; its text is 10 pt (Source Sans 3:
|
||||
// typo ascender 1 em).
|
||||
let line = |text: &str, list: ListType| {
|
||||
let (mut d, sid, _) = doc_with(text, Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs { list_type: Some(list), ..Default::default() });
|
||||
let st = d.story_mut(sid).unwrap();
|
||||
for p in &mut st.paras {
|
||||
p.chars = CharAttrs { font_family: Some("Source Sans 3".into()), size: Some(40.0), ..Default::default() };
|
||||
}
|
||||
st.format_chars(0..text.len(), |f| f.over.size = Some(10.0));
|
||||
let l = compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0].clone();
|
||||
(l.baseline, l.ascent, l.leading)
|
||||
};
|
||||
let (b, asc, lead) = line("Hxg Hxg", ListType::None);
|
||||
// 1 em of the 10 pt text below the frame top.
|
||||
assert!((b - 46.0).abs() < 0.01, "{b}");
|
||||
for list in [ListType::Bullets, ListType::Numbers] {
|
||||
let (lb, lasc, llead) = line("Hxg Hxg", list);
|
||||
assert!((lb - b).abs() < 1e-6, "{list:?}: {lb} vs {b}");
|
||||
// The 40 pt label still sets the line's ascent and leading.
|
||||
assert!(lasc > asc + 20.0 && (llead - 48.0).abs() < 1e-6 && (lead - 12.0).abs() < 1e-6, "{list:?}: {lasc} {asc} {llead} {lead}");
|
||||
}
|
||||
// An empty bulleted paragraph sits like an empty paragraph.
|
||||
let (eb, _, _) = line("", ListType::None);
|
||||
let (ebl, _, _) = line("", ListType::Bullets);
|
||||
assert!((ebl - eb).abs() < 1e-6, "{ebl} vs {eb}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaces_count_toward_the_ascent_first_baseline() {
|
||||
// 10 pt text with a 40 pt space between its words (Source Sans 3: typo ascender 1 em).
|
||||
let (mut d, sid, _) = doc_with("Hxg Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
let st = d.story_mut(sid).unwrap();
|
||||
st.format_chars(0..7, |f| {
|
||||
f.over.font_family = Some("Source Sans 3".into());
|
||||
f.over.size = Some(10.0);
|
||||
});
|
||||
st.format_chars(3..4, |f| f.over.size = Some(40.0));
|
||||
let l = &compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0];
|
||||
assert!((l.baseline - (36.0 + 40.0)).abs() < 0.01, "{}", l.baseline);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_scale_does_not_move_the_ascent_first_baseline() {
|
||||
let db = designcraft_fonts::FontDb::global();
|
||||
db.add_font(typo_test_font("TypoAscentVSc", Some(700)));
|
||||
let line = |v_scale: f64| {
|
||||
let (mut d, sid, fid) = doc_with("Hxg", Rect::new(36.0, 36.0, 300.0, 300.0), ParaAttrs::default());
|
||||
d.story_mut(sid).unwrap().format_chars(0..3, |f| {
|
||||
f.over.font_family = Some("TypoAscentVSc".into());
|
||||
f.over.size = Some(20.0);
|
||||
f.over.v_scale = Some(v_scale);
|
||||
});
|
||||
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.inset = [4.0, 0.0, 0.0, 0.0];
|
||||
let l = &compose_story(&d, sid, &ComposeOptions::default()).frames[0].lines[0];
|
||||
(l.baseline, l.ascent)
|
||||
};
|
||||
let ((b100, a100), (b200, a200)) = (line(1.0), line(2.0));
|
||||
// The typo ascender of the nominal size (0.7 × 20 pt) at 100 % and at 200 %.
|
||||
assert!((b100 - (36.0 + 4.0 + 14.0)).abs() < 0.01, "{b100}");
|
||||
assert!((b200 - b100).abs() < 0.01, "{b200} vs {b100}");
|
||||
// The line box keeps the scaled ascent.
|
||||
assert!((a100 - 20.0).abs() < 0.01 && (a200 - 40.0).abs() < 0.01, "{a100} {a200}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cap_height_and_x_height_first_baselines_use_the_fonts_cap_and_x_heights() {
|
||||
use designcraft_doc::FirstBaseline;
|
||||
@@ -2758,3 +2842,29 @@ fn vertical_lines_fit_the_em_box() {
|
||||
assert!((l.baseline - col.y0 - ascent).abs() < 1e-9, "{family}: {} {}", l.baseline, col.y0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A drop cap set in one of the document's own fonts measures its side bearing in that font.
|
||||
#[test]
|
||||
fn drop_cap_in_a_document_font_aligns_its_left_edge() {
|
||||
use designcraft_geom::Shape as _;
|
||||
const FAMILY: &str = "DocFont Drop Cap";
|
||||
let text = format!("Once {LOREM}");
|
||||
let mut chars: Vec<char> = text.chars().collect();
|
||||
chars.sort_unstable();
|
||||
chars.dedup();
|
||||
let folder = std::env::temp_dir().join(format!("dc-compose-dropcap-docfont-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&folder).unwrap();
|
||||
std::fs::write(folder.join("dropcap.ttf"), designcraft_fonts::testing::font_with_glyph(FAMILY, &chars, 'O').unwrap()).unwrap();
|
||||
let scope = designcraft_fonts::FontDb::global().load_document_fonts(&folder).scope;
|
||||
let para = ParaAttrs { left_indent: Some(10.0), ..drop_cap(2, 1) };
|
||||
let (mut d, sid, _) = doc_with(&text, Rect::new(0.0, 0.0, 300.0, 1000.0), para);
|
||||
d.font_scope = scope;
|
||||
d.story_mut(sid).unwrap().format_chars(0..text.len(), |f| f.over.font_family = Some(FAMILY.into()));
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let dc = &all_lines(&cs)[0].glyphs[0];
|
||||
assert_eq!(dc.face.family, FAMILY);
|
||||
let ink_left = dc.x + designcraft_fonts::FontDb::global().outline(dc.face.get(), dc.gid).bounding_box().x0 * dc.sx;
|
||||
assert!(ink_left - dc.x > 0.5, "the O has a visible side bearing");
|
||||
assert!((ink_left - 10.0).abs() < 1e-6, "ink at {ink_left}");
|
||||
let _ = std::fs::remove_dir_all(&folder);
|
||||
}
|
||||
|
||||
+44
-2
@@ -1141,13 +1141,29 @@ impl Item {
|
||||
b.inflate(w, w)
|
||||
}
|
||||
|
||||
/// The frame's text area (inner space) after inset.
|
||||
/// The text frame's text area (inner space): inside the part of a visible stroke that lies
|
||||
/// within the frame, then the inset, on each edge.
|
||||
pub fn text_area(&self) -> Rect {
|
||||
let r = self.inner_bounds();
|
||||
let inset = self.text_frame().map(|t| t.options.inset).unwrap_or([0.0; 4]);
|
||||
let s = self.stroke_inside();
|
||||
let inset = self.text_frame().map(|t| t.options.inset).unwrap_or([0.0; 4]).map(|v| v + s);
|
||||
Rect::new(r.x0 + inset[1], r.y0 + inset[0], (r.x1 - inset[3]).max(r.x0 + inset[1]), (r.y1 - inset[2]).max(r.y0 + inset[0]))
|
||||
}
|
||||
|
||||
/// How far a visible stroke reaches inside the path: half its weight centred, all of it inside,
|
||||
/// none outside.
|
||||
fn stroke_inside(&self) -> f64 {
|
||||
let w = self.stroke.weight;
|
||||
if self.stroke.is_none() || !w.is_finite() {
|
||||
return 0.0;
|
||||
}
|
||||
match self.stroke.align {
|
||||
StrokeAlign::Center => w / 2.0,
|
||||
StrokeAlign::Inside => w,
|
||||
StrokeAlign::Outside => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spread-space centre.
|
||||
pub fn center(&self) -> Point {
|
||||
self.bounds().center()
|
||||
@@ -1192,6 +1208,32 @@ mod tests {
|
||||
assert_eq!(it.visible_bounds(), Rect::new(8.0, 18.0, 112.0, 72.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_area_starts_inside_the_stroke() {
|
||||
let mut it = Item::new(ItemId(1), LayerId(1), Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)));
|
||||
it.content = Content::Text(TextFrame { story: StoryId(2), options: TextFrameOptions { inset: [5.0, 6.0, 7.0, 8.0], ..Default::default() } });
|
||||
let unstroked = Rect::new(6.0, 5.0, 92.0, 43.0);
|
||||
let stroked = |align: StrokeAlign, swatch: &str| {
|
||||
let mut it = it.clone();
|
||||
it.stroke = Stroke { weight: 4.0, align, swatch: swatch.into(), ..Stroke::default() };
|
||||
it.text_area()
|
||||
};
|
||||
let black = designcraft_color::swatch::BLACK;
|
||||
// The part of the stroke inside the frame, on every edge, then the inset.
|
||||
assert_eq!(stroked(StrokeAlign::Center, black), Rect::new(8.0, 7.0, 90.0, 41.0));
|
||||
assert_eq!(stroked(StrokeAlign::Inside, black), Rect::new(10.0, 9.0, 88.0, 39.0));
|
||||
assert_eq!(stroked(StrokeAlign::Outside, black), unstroked);
|
||||
// A stroke without a colour doesn't count.
|
||||
assert_eq!(stroked(StrokeAlign::Inside, designcraft_color::swatch::NONE), unstroked);
|
||||
// Nor does a weight that isn't a number.
|
||||
it.stroke = Stroke { weight: f64::NAN, ..Stroke::default() };
|
||||
assert_eq!(it.text_area(), unstroked);
|
||||
// A stroke wider than the frame leaves no room for text.
|
||||
it.stroke = Stroke { weight: 200.0, align: StrokeAlign::Inside, ..Stroke::default() };
|
||||
let a = it.text_area();
|
||||
assert!(a.width() == 0.0 && a.height() == 0.0, "{a:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_bounds_union_children() {
|
||||
let a = Item::new(ItemId(1), LayerId(1), Shape::Rectangle, shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)));
|
||||
|
||||
@@ -134,6 +134,34 @@ fn removing_a_frame_from_its_thread_keeps_the_text_in_the_others() {
|
||||
assert!(d.remove_from_thread(b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_removed_from_a_vertical_thread_stays_vertical() {
|
||||
let mut d = doc();
|
||||
let lid = d.default_layer();
|
||||
let (a, sa) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(10.0, 10.0, 100.0, 100.0), lid, "縦書き", ParaFormat::default()).unwrap();
|
||||
let (b, _) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(110.0, 10.0, 200.0, 100.0), lid, "", ParaFormat::default()).unwrap();
|
||||
d.story_mut(sa).unwrap().vertical = true;
|
||||
d.thread(a, b).unwrap();
|
||||
d.remove_from_thread(b).unwrap();
|
||||
d.check().unwrap();
|
||||
assert!(d.frame_vertical(d.item(a).unwrap()));
|
||||
assert!(d.frame_vertical(d.item(b).unwrap()), "the removed frame keeps the story's direction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_frame_removed_from_a_right_to_left_thread_keeps_its_column_order() {
|
||||
let mut d = doc();
|
||||
let lid = d.default_layer();
|
||||
let (a, sa) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(10.0, 10.0, 100.0, 100.0), lid, "نص", ParaFormat::default()).unwrap();
|
||||
let (b, _) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(110.0, 10.0, 200.0, 100.0), lid, "", ParaFormat::default()).unwrap();
|
||||
d.story_mut(sa).unwrap().direction = crate::TextDirection::RightToLeft;
|
||||
d.thread(a, b).unwrap();
|
||||
d.remove_from_thread(b).unwrap();
|
||||
d.check().unwrap();
|
||||
let own = d.item(b).unwrap().text_frame().unwrap().story;
|
||||
assert_eq!(d.story(own).unwrap().direction, crate::TextDirection::RightToLeft);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frames_keep_their_story_direction_through_threading() {
|
||||
let mut d = doc();
|
||||
|
||||
@@ -900,4 +900,24 @@ mod document_fonts_tests {
|
||||
assert!(glyph_faces(&s).iter().all(|(_, f)| f.family == FAMILY));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bold_and_italic_link_the_document_fonts_styles() {
|
||||
const FAMILY: &str = "DocFont Linked";
|
||||
let dir = temp_dir("linked");
|
||||
std::fs::write(dir.join(DOCUMENT_FONTS_FOLDER).join("linked.ttf"), font_with(FAMILY, &['H', 'e', 'l', 'o']).unwrap()).unwrap();
|
||||
let path = dir.join("Linked.designcraft");
|
||||
write_document(&path, FAMILY);
|
||||
|
||||
let mut s = Session::new();
|
||||
open(&mut s, &path);
|
||||
let sid = *s.doc().unwrap().doc.stories.keys().next().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid.0, "anchor": 0, "focus": 0})).unwrap();
|
||||
// The family is the document's alone: its Regular is the style without bold or italic.
|
||||
let r = s.execute("type.bold", &json!({"on": false})).unwrap();
|
||||
assert_eq!(r["fontStyle"], "Regular", "{r}");
|
||||
let r = s.execute("type.italic", &json!({"on": false})).unwrap();
|
||||
assert_eq!(r["fontStyle"], "Regular", "{r}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1360,9 +1360,6 @@ impl<'a> Ex<'a> {
|
||||
if let Some(v) = a.single_word_justify {
|
||||
el.set("SingleWordJustification", names::align_out(v));
|
||||
}
|
||||
if let Some(v) = a.kashidas {
|
||||
el.set("Kashidas", names::kashidas_out(v));
|
||||
}
|
||||
n!(keep_with_next, "KeepWithNext");
|
||||
b!(keep_lines_together, "KeepLinesTogether");
|
||||
b!(keep_all_lines, "KeepAllLinesTogether");
|
||||
|
||||
@@ -1286,7 +1286,6 @@ impl<'r> Importer<'r> {
|
||||
a.glyph_scale_max = frac("MaximumGlyphScaling");
|
||||
a.auto_leading = frac("AutoLeading");
|
||||
a.single_word_justify = e.prop("SingleWordJustification").and_then(|v| names::align_in(v.trim()));
|
||||
a.kashidas = e.prop("Kashidas").and_then(|v| names::kashidas_in(v.trim()));
|
||||
a.keep_with_next = u("KeepWithNext");
|
||||
a.keep_lines_together = e.boolean("KeepLinesTogether");
|
||||
a.keep_all_lines = e.boolean("KeepAllLinesTogether");
|
||||
|
||||
@@ -447,13 +447,6 @@ pub fn auto_size_in(s: &str) -> AutoSize {
|
||||
.find(|m| auto_size_out(*m) == s)
|
||||
.unwrap_or(AutoSize::Off)
|
||||
}
|
||||
/// Insert Kashidas (`Kashidas`): InDesign's default, or off.
|
||||
pub fn kashidas_out(on: bool) -> &'static str {
|
||||
if on { "DefaultKashidas" } else { "KashidasOff" }
|
||||
}
|
||||
pub fn kashidas_in(s: &str) -> Option<bool> {
|
||||
[true, false].into_iter().find(|on| kashidas_out(*on) == s)
|
||||
}
|
||||
|
||||
/// Auto-size reference points in our 0..9 order (row-major, top-left first).
|
||||
pub const REF_POINTS: [&str; 9] = [
|
||||
|
||||
@@ -1289,8 +1289,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
// Bold and Italic pick the family's own styles (style linking).
|
||||
let (weight, italic) =
|
||||
designcraft_fonts::FontDb::global().traits_of(c["fontFamily"].as_str().unwrap_or_default(), c["fontStyle"].as_str().unwrap_or_default());
|
||||
let (weight, italic) = super::fonts(app).traits_of(c["fontFamily"].as_str().unwrap_or_default(), c["fontStyle"].as_str().unwrap_or_default());
|
||||
let tip = |app: &DesignApp, name: &str, id: &str| {
|
||||
let name = crate::i18n::tr(&app.ui.language, name);
|
||||
match crate::menus::shortcut_of(app, id) {
|
||||
|
||||
Reference in New Issue
Block a user