Show Hidden Characters marks table cells, as InDesign

Every text cell now shows its own hidden characters: spaces, tabs, paragraph
returns and an end-of-story # after its text (an empty cell shows just the #).
The table paragraph's return sits after the table's bottom right corner.
Cells showed no marks at all, so an empty table looked like plain lines.

The table marks come from table_hidden_marks (frame space), drawn with the
story's own marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
moppeeerandClaude Opus 5.5 committed 2026-10-23 21:31:06 +02:00
1 parent 2e36dcf909
commit cb988aef83
2 files changed
+58 -8

No files matched your search

+1
View File
@@ -349,6 +349,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
- **2026-10-23** · Show Hidden Characters marks table cells as InDesign does: every cell shows its own spaces, tabs and paragraph returns and an end-of-story # after its text (an empty cell shows just the #), and the table paragraph's ¶ sits after the table's bottom right corner. Cells showed no marks. No readiness percentage change.
- **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change.
- **2026-10-22** · macOS: the app menu has a Language submenu after Settings… with each interface language in its own name and the current one checked, as in the suite's other apps; Preferences › General › Interface Language stays. No readiness percentage change.
- **2026-10-22** · macOS menu bar: the menus keep working after the interface language or Show CJK Features changes (a rebuilt menu bar lost every click, Quit included), and its key equivalents are the shortcuts in effect, so a shortcut moved in the Keyboard Shortcuts editor leaves its old menu item. No readiness percentage change.
+57 -8
View File
@@ -2744,6 +2744,8 @@ fn ime_output(app: &mut DesignApp, ctx: &egui::Context, xf: &Xf) {
}
/// View → Show Hidden Characters: ¶ paragraph ends, » tabs, · spaces, ¬ forced line breaks, # end of story.
/// As InDesign, every table cell shows its own marks (an empty cell its #), and a table's
/// paragraph return sits after the table's bottom right corner.
fn draw_hidden_characters(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document, layout: &CanvasLayout) {
for story in doc.stories.values() {
let cs = app.session.cache.get(doc, story.id, None);
@@ -2751,19 +2753,39 @@ fn draw_hidden_characters(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc
let (Some((a, _)), Some(it)) = (item_canvas_xf(doc, layout, ft.frame), doc.item(ft.frame)) else { continue };
let m = a * doc.text_xf(it);
let col = layer_color(doc, it);
for l in &ft.lines {
let size = (l.ascent * 0.75 * xf.zoom).clamp(6.0, 40.0) as f32;
let font = egui::FontId::proportional(size);
for (x, mark, centered) in hidden_marks(&story.text, l) {
let p = xf.to_screen(m * Point::new(x, l.baseline));
let align = if centered { egui::Align2::CENTER_BOTTOM } else { egui::Align2::LEFT_BOTTOM };
painter.text(p, align, mark, font.clone(), col);
}
let lines = ft
.lines
.iter()
.flat_map(|l| hidden_marks(&story.text, l).into_iter().map(move |(x, mark, c)| (Point::new(x, l.baseline), mark, c, l.ascent)));
for (p, mark, centered, ascent) in lines.chain(ft.tables.iter().flat_map(table_hidden_marks)) {
let size = (ascent * 0.75 * xf.zoom).clamp(6.0, 40.0) as f32;
let align = if centered { egui::Align2::CENTER_BOTTOM } else { egui::Align2::LEFT_BOTTOM };
painter.text(xf.to_screen(m * p), align, mark, egui::FontId::proportional(size), col);
}
}
}
}
/// A table fragment's hidden-character marks in frame space (position at the baseline, mark,
/// centered, the line's ascent): each text cell's own marks, its # where its text ends, and the
/// table paragraph's ¶ after the last row's bottom right corner.
fn table_hidden_marks(t: &compose::TableFrag) -> Vec<(Point, &'static str, bool, f64)> {
let mut out = Vec::new();
for c in t.cells.iter().filter(|c| c.graphic.is_none()) {
for l in c.text.frames.first().map_or(&[][..], |f| &f.lines) {
out.extend(
hidden_marks(&c.source, l)
.into_iter()
.map(|(x, mark, centered)| (c.origin + designcraft_geom::Vec2::new(x, l.baseline), mark, centered, l.ascent)),
);
}
}
if t.last {
out.push((Point::new(t.rect.x1 + 1.0, t.rect.y1), "", false, 12.0));
}
out
}
/// The hidden-character marks on line `l` of a story with text `text`: frame-space x, mark, and
/// whether the mark is centered on x (spaces) or starts at it.
fn hidden_marks(text: &str, l: &compose::Line) -> Vec<(f64, &'static str, bool)> {
@@ -5668,6 +5690,33 @@ mod tests {
assert!(para_x >= space.x + space.adv, "{mark} at {para_x} overlaps the last space");
}
}
/// As InDesign: an end-of-story # in every cell (after its text), a ¶ where a cell paragraph
/// ends, and the table paragraph's ¶ just after the table's bottom right corner.
#[test]
fn table_cells_show_their_hidden_characters() {
let mut d = Document::new(&designcraft_doc::build::NewDocument::default());
let lid = d.default_layer();
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), DRect::new(0.0, 0.0, 300.0, 300.0), lid, "", Default::default()).unwrap();
let data = vec![vec!["Name:".to_string(), "".into()], vec!["a\nb".into(), "".into()]];
let t = designcraft_doc::Table::from_strings(d.alloc(), &data, 200.0, &Default::default(), &Default::default());
d.story_mut(sid).unwrap().insert_table(0, t);
let cs = compose::compose_story(&d, sid, &compose::ComposeOptions::default());
let frag = &cs.frames[0].tables[0];
let marks = table_hidden_marks(frag);
let ends = |m: &str| marks.iter().filter(|x| x.1 == m).count();
assert_eq!(ends("#"), 4, "one per cell: {marks:?}");
assert_eq!(ends(""), 2, "the cell's first paragraph and the table's: {marks:?}");
// Each # is inside its cell, after the cell's text.
for c in &frag.cells {
let inside = marks.iter().filter(|m| m.1 == "#" && c.rect.contains(m.0)).count();
assert_eq!(inside, 1, "cell {},{}: {marks:?}", c.row, c.col);
}
let name = frag.cell(0, 0).unwrap();
assert!(marks.iter().any(|m| m.1 == "#" && name.rect.contains(m.0) && m.0.x > name.origin.x + 20.0), "after Name:");
let last = marks.last().unwrap();
assert_eq!((last.0, last.1), (designcraft_geom::Point::new(frag.rect.x1 + 1.0, frag.rect.y1), ""));
}
}
#[cfg(test)]