mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 12:46:53 -06:00
Merge section-marker: Sections: the section marker shows the section's marker text (#556)
Conflict: ROADMAP.md: both progress lines (next's, then #556's) Conflict: crates/compose/src/shape.rs: #556's empty-marker zero-width glyph; next's digit mapping, styles.at(i)/styles and sub.upright() in both arms
This commit is contained in:
@@ -137,6 +137,7 @@ Newest first.
|
||||
readiness percentage change.
|
||||
- **2026-10-12** · Properties › Transform shows rotation, shear and X/Y scale, and Rotate 90° and Flip Horizontal/Vertical buttons, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-12** · Underlines and strikethroughs are drawn as one bar per stretch of text on screen, as in PDF export: a bar per letter left faint seams between the letters. No readiness percentage change.
|
||||
- **2026-10-12** · The section marker character shows its section's marker text on document pages (nothing when the section has none) instead of the word "Section", which stays on parent pages; editing the marker recomposes (#556). No readiness percentage change.
|
||||
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
|
||||
- **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows selects matching or default driver paper and shrinks oversized sheets without enlarging smaller ones; queue duplex defaults are preserved. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows CI passes, including paused-queue submission and cleanup. PostScript, production print controls and physical output validation remain open. No readiness percentage change.
|
||||
- **2026-10-11** · The View menu follows InDesign's: proofing first, then zoom, Rotate Spread, Screen Mode, Display Performance, rulers, Extras, Grids & Guides and Structure; the view items that sat at the end of the Type menu moved there. No readiness percentage change.
|
||||
|
||||
@@ -67,10 +67,18 @@ impl Cache {
|
||||
// The key holds the page's index, which a renumbering doesn't change: its name (section
|
||||
// start number, style, prefix) is part of the signature.
|
||||
let page_name = page.map(|p| doc.page_name(p));
|
||||
let section_marker = page.map(|p| crate::section_marker(doc, p));
|
||||
if let Some(name) = &page_name {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
name.hash(&mut h);
|
||||
(name, §ion_marker).hash(&mut h);
|
||||
sig.push(h.finish() as usize);
|
||||
}
|
||||
if story.text.contains(designcraft_doc::story::SECTION_MARKER) {
|
||||
// Frames on document pages show their section's marker text.
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
doc.sections.iter().for_each(|s| (s.start, &s.marker).hash(&mut h));
|
||||
sig.push(h.finish() as usize);
|
||||
}
|
||||
let with_vars = crate::vars::has_vars(story);
|
||||
@@ -146,7 +154,7 @@ impl Cache {
|
||||
let out = Arc::new(crate::compose_story(
|
||||
doc,
|
||||
sid,
|
||||
&ComposeOptions { page_name, page, running, label: None, xrefs: with_xrefs.then(|| self.xref_index(doc)) },
|
||||
&ComposeOptions { page_name, section_marker, page, running, label: None, xrefs: with_xrefs.then(|| self.xref_index(doc)) },
|
||||
));
|
||||
let mut g = self.map.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let stamp = g.1;
|
||||
|
||||
@@ -485,6 +485,8 @@ pub struct FrameSpec {
|
||||
pub vertical: bool,
|
||||
pub exclusions: Vec<Exclusion>,
|
||||
pub page_name: Option<String>,
|
||||
/// Marker text of the section the frame's page is in (None on parent pages).
|
||||
pub section_marker: Option<String>,
|
||||
/// Absolute document page the frame is on (None on parent pages).
|
||||
pub page: Option<usize>,
|
||||
/// Baseline grid in inner space: (first grid line y, increment).
|
||||
@@ -511,11 +513,18 @@ impl FrameSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/// The marker text of the section absolute page `abs` is in (empty when it has none).
|
||||
pub fn section_marker(doc: &Document, abs: usize) -> String {
|
||||
doc.section_of(abs).map(|s| s.marker.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Composition inputs that are not in the story itself.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ComposeOptions {
|
||||
/// Page name for page-number markers when the story is composed for a specific page (parent items).
|
||||
pub page_name: Option<String>,
|
||||
/// Section marker text when the story is composed for a specific page (parent items).
|
||||
pub section_marker: Option<String>,
|
||||
/// Absolute page for text variables when composed for a specific page (parent items).
|
||||
pub page: Option<usize>,
|
||||
/// Running-header index (needed when the story contains running-header variables).
|
||||
@@ -592,6 +601,7 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
}
|
||||
_ => (None, None, false),
|
||||
};
|
||||
let section_marker = page.map(|abs| section_marker(doc, abs));
|
||||
// The page under the frame and its margins, in inner space (bounding boxes).
|
||||
let page_rect = match (spread_ref, spread) {
|
||||
(_, Some(sp)) => sp
|
||||
@@ -650,6 +660,7 @@ pub fn frame_specs(doc: &Document, sid: StoryId) -> Vec<FrameSpec> {
|
||||
vertical,
|
||||
exclusions: if tf.options.path.is_some() { vec![] } else { exclusions },
|
||||
page_name,
|
||||
section_marker,
|
||||
page,
|
||||
grid,
|
||||
left_page,
|
||||
@@ -835,7 +846,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
};
|
||||
let mut sub = SubstCtx {
|
||||
page_name: opts.page_name.clone().or_else(|| cur_frame.and_then(|f| f.page_name.clone())),
|
||||
section_marker: None,
|
||||
section_marker: opts.section_marker.clone().or_else(|| cur_frame.and_then(|f| f.section_marker.clone())),
|
||||
vars: var_values,
|
||||
hidden_conditions: doc.conditions.iter().filter(|c| !c.visible).map(|c| c.name.clone()).collect(),
|
||||
vertical: cur_frame.is_some_and(|f| f.vertical),
|
||||
|
||||
@@ -102,6 +102,7 @@ impl Notes {
|
||||
vertical: false,
|
||||
exclusions: vec![],
|
||||
page_name: f.page_name.clone(),
|
||||
section_marker: f.section_marker.clone(),
|
||||
page: f.page,
|
||||
grid: None,
|
||||
left_page: f.left_page,
|
||||
@@ -109,6 +110,7 @@ impl Notes {
|
||||
};
|
||||
let nopts = ComposeOptions {
|
||||
page_name: opts.page_name.clone(),
|
||||
section_marker: opts.section_marker.clone(),
|
||||
page: opts.page,
|
||||
running: opts.running.clone(),
|
||||
label: Some(label),
|
||||
|
||||
@@ -171,6 +171,7 @@ fn shaping_key(p: &CharProps) -> CharProps {
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SubstCtx {
|
||||
pub page_name: Option<String>,
|
||||
/// The section marker text of the page being composed (`None` on parent pages: "Section").
|
||||
pub section_marker: Option<String>,
|
||||
/// Text variable values by index (on the page being composed).
|
||||
pub vars: Arc<Vec<String>>,
|
||||
@@ -871,9 +872,16 @@ fn shape_run_raw(
|
||||
} else {
|
||||
sub.page_name.clone().unwrap_or_else(|| "#".into())
|
||||
};
|
||||
// Page numbers take the digits of the text they stand in.
|
||||
let s: String = s.chars().map(|ch| digits.map(ch, &p.language)).collect();
|
||||
shape_segment(db, text, i..i + c.len_utf8(), Some(&s), p, &primary, auto_leading, styles, out, sub.upright());
|
||||
if s.is_empty() {
|
||||
// A section without marker text: the character takes no space.
|
||||
let mut g = control_glyph(&primary, p, auto_leading, styles.at(i), i, c);
|
||||
g.adv = 0.0;
|
||||
out.push(g);
|
||||
} else {
|
||||
// Page numbers take the digits of the text they stand in.
|
||||
let s: String = s.chars().map(|ch| digits.map(ch, &p.language)).collect();
|
||||
shape_segment(db, text, i..i + c.len_utf8(), Some(&s), p, &primary, auto_leading, styles, out, sub.upright());
|
||||
}
|
||||
}
|
||||
designcraft_doc::OBJECT_MARK => {
|
||||
let mut g = control_glyph(&primary, p, auto_leading, styles.at(i), i, c);
|
||||
|
||||
@@ -129,6 +129,7 @@ fn compose_cell(doc: &Document, cell: &Cell, w: f64, opts: &ComposeOptions, left
|
||||
vertical: false,
|
||||
exclusions: vec![],
|
||||
page_name: opts.page_name.clone(),
|
||||
section_marker: opts.section_marker.clone(),
|
||||
page: opts.page,
|
||||
grid: None,
|
||||
left_page,
|
||||
|
||||
@@ -153,6 +153,38 @@ mod tests {
|
||||
assert_eq!(shown(&s, 2), 3, "page 102 after renumbering");
|
||||
}
|
||||
|
||||
/// A section marker character shows the marker text of the section of the page it is composed
|
||||
/// for: on each document page a parent-page frame is shown on, and in a frame on a document
|
||||
/// page. A section without marker text shows nothing; the parent page itself shows "Section".
|
||||
#[test]
|
||||
fn section_marker_shows_the_pages_section_marker() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 4, "facingPages": false})).unwrap();
|
||||
s.execute("layout.section", &json!({"page": 3, "startNumber": 1, "marker": "Intro"})).unwrap();
|
||||
let marker = designcraft_doc::story::SECTION_MARKER.to_string();
|
||||
let r = s
|
||||
.execute("frame.create", &json!({"spread": {"kind": "parent", "index": 0}, "rect": [72, 30, 500, 60], "content": "text", "text": marker}))
|
||||
.unwrap();
|
||||
let parent = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story;
|
||||
let sp = s.doc().unwrap().doc.page_loc(3).unwrap().0;
|
||||
let r = s.execute("frame.create", &json!({"spread": sp, "rect": [72, 200, 400, 260], "content": "text", "text": marker})).unwrap();
|
||||
let own = s.doc().unwrap().doc.item(designcraft_doc::ItemId(r["id"].as_u64().unwrap())).unwrap().text_frame().unwrap().story;
|
||||
let shown = |s: &Session, sid, page: Option<usize>| -> usize {
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let cs = s.cache.get(&d, sid, page);
|
||||
cs.frames.iter().flat_map(|f| f.lines.iter()).map(|l| l.glyphs.iter().filter(|g| g.visible && g.adv > 0.0).count()).sum()
|
||||
};
|
||||
assert_eq!(shown(&s, parent, Some(0)), 0, "the first section has no marker");
|
||||
assert_eq!(shown(&s, parent, Some(2)), "Intro".len());
|
||||
assert_eq!(shown(&s, parent, Some(3)), "Intro".len());
|
||||
assert_eq!(shown(&s, own, None), "Intro".len(), "a frame on a document page");
|
||||
assert_eq!(shown(&s, parent, None), "Section".len(), "the parent page itself");
|
||||
// Editing the marker recomposes.
|
||||
s.execute("layout.section", &json!({"page": 3, "marker": "Appendix"})).unwrap();
|
||||
assert_eq!(shown(&s, parent, Some(2)), "Appendix".len());
|
||||
assert_eq!(shown(&s, own, None), "Appendix".len());
|
||||
}
|
||||
|
||||
/// Two sections that both number from 1 repeat page names ("1, 2, 1, 2"). A parent-page running
|
||||
/// header is composed for the page it is shown on, not the first page with that name.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user