C: Appearance of Black on screen (rich black / accurate)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-02 16:59:19 -04:00
parent 53fc678978
commit bfe4220840
7 changed files with 73 additions and 3 deletions

View File

@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
- Preferences › Appearance of Black: 100% K on screen accurately (dark grey) or as rich black (`window.richBlack`); the printing/export side is still to come.
- Spreads of up to 10 pages: Add to Previous Spread and Allow Spread to Shuffle (Pages panel; `layout.pages.toSpread`, `layout.spreadShuffle`); inserted pages go around kept spreads.
- Page tool (⇧P) and `layout.pageSize`: pages of their own size (or a preset); the spine stays put and objects keep their place on their page.
- File › Print (⌘P): printer, copies, page range, spreads, marks, bleed; printed as PDF through the system spooler (`file.print`, `file.printers`, `dryRun` for agents). Shortcuts now act like choosing the menu item (dialogs open), and UI commands win shortcut ties (⌘N opens New Document).

View File

@ -102,6 +102,9 @@ pub struct RenderOptions {
pub highlight_missing_fonts: bool,
/// View › Display Performance.
pub quality: DisplayQuality,
/// Preferences › Appearance of Black: show 100% K as rich (pure) black instead of the
/// accurate dark grey.
pub rich_black: bool,
}
/// View › Display Performance: how placed graphics and effects are drawn on screen.
@ -128,6 +131,7 @@ impl Default for RenderOptions {
page_shadow: false,
highlight_missing_fonts: false,
quality: DisplayQuality::High,
rich_black: false,
}
}
}
@ -237,6 +241,7 @@ impl Renderer {
_ => RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: self.threads, ..Default::default() }),
};
let mt = ctx.render_settings().num_threads > 0;
RICH_BLACK.with(|r| r.set(opts.rich_black));
self.stats = FrameStats::default();
self.stories.clear();
self.glyphs.tick();
@ -582,7 +587,21 @@ fn rect_overlaps(a: Rect, b: Rect) -> bool {
a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0
}
thread_local! {
/// Appearance of Black for the render on this thread: 100% K as pure black.
static RICH_BLACK: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub fn color_of(c: &designcraft_color::Color, alpha: f32) -> peniko::Color {
if RICH_BLACK.with(|r| r.get())
&& let designcraft_color::Color::Cmyk { c: cc, m, y, k } = *c
&& cc <= 1e-3
&& m <= 1e-3
&& y <= 1e-3
&& k >= 0.999
{
return peniko::Color::from_rgba8(0, 0, 0, (alpha.clamp(0.0, 1.0) * 255.0).round() as u8);
}
let [r, g, b, a] = c.to_rgba8(alpha);
peniko::Color::from_rgba8(r, g, b, a)
}
@ -832,6 +851,25 @@ mod tests {
assert!(gap[0] < 80 && gap[2] > 150, "gap {gap:?}");
}
#[test]
fn appearance_of_black_on_screen() {
let mut d = Document::new(&NewDocument::default());
let lid = d.default_layer();
let id = designcraft_doc::ItemId(d.alloc());
let mut it =
Item::new(id, lid, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(Rect::new(100.0, 100.0, 200.0, 200.0)));
it.fill = Fill::swatch(designcraft_color::swatch::BLACK);
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let cache = Cache::new();
let mut r = Renderer::new();
r.threads = 0;
let px = |rich: bool, r: &mut Renderer| {
r.render_page(&d, &cache, 0, 1.0, false, &RenderOptions { rich_black: rich, ..Default::default() }).unwrap().pixel(150, 150)
};
assert!(px(false, &mut r)[0] > 20, "accurate: a dark grey");
assert_eq!(px(true, &mut r)[..3], [0, 0, 0]);
}
#[test]
fn display_performance_fast_draws_grey_boxes() {
let mut d = Document::new(&NewDocument::default());

View File

@ -312,6 +312,7 @@ fn render_texture(app: &mut DesignApp, ctx: &egui::Context, rect: Rect, xf: &Xf,
greek_below_px: 3.0 * ppp,
highlight_missing_fonts: !preview,
quality: app.ui.display_quality,
rich_black: app.ui.rich_black,
..Default::default()
};
(st.doc.clone(), placed, w, h, view, opts)
@ -415,6 +416,7 @@ fn patch_texture(app: &mut DesignApp, layout: &CanvasLayout, ppp: f64, doc_key:
greek_below_px: 3.0 * ppp,
highlight_missing_fonts: !preview,
quality: app.ui.display_quality,
rich_black: app.ui.rich_black,
..Default::default()
};
let img = app.canvas.patcher.render(&new, &app.session.cache, &placed, (x1 - x0) as u32, (y1 - y0) as u32, view, &opts);

View File

@ -343,6 +343,7 @@ fn preferences(ui: &mut egui::Ui, d: &mut Dialog) {
("grids", "Grids"),
("guides", "Guides & Pasteboard"),
("display", "Display Performance"),
("black", "Appearance of Black"),
]
} else {
&[("general", "General"), ("interface", "Interface"), ("type", "Type"), ("display", "Display Performance")]
@ -371,6 +372,23 @@ fn preferences(ui: &mut egui::Ui, d: &mut Dialog) {
ui.label(egui::RichText::new("Smart Text Reflow").font(semibold(12.0)));
check(ui, d, "smartTextReflow", "Add and remove pages as the primary text frame's story grows and shrinks");
}
"black" => {
ui.label(egui::RichText::new("Options for Black on RGB and Grayscale Devices").font(semibold(12.0)));
ui.horizontal(|ui| {
ui.label("On Screen:");
let rich = d.b("richBlack");
egui::ComboBox::from_id_salt("pref_black")
.selected_text(if rich { "Display All Blacks as Rich Black" } else { "Display All Blacks Accurately" })
.width(240.0)
.show_ui(ui, |ui| {
for (v, l) in [(false, "Display All Blacks Accurately"), (true, "Display All Blacks as Rich Black")] {
if ui.selectable_label(v == rich, l).clicked() {
d.fields.insert("richBlack".into(), json!(v));
}
}
});
});
}
"interface" => {
ui.label(egui::RichText::new("UI Scaling").font(semibold(12.0)));
ui.horizontal(|ui| {
@ -862,6 +880,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
_ => "view.highQualityDisplay",
};
app.run(view, json!({}))?;
app.run("window.richBlack", json!({"on": d.b("richBlack")}))?;
if let Some(v) = d.n("uiScale") {
app.run("window.uiScale", json!({"scale": v / 100.0}))?;
}

View File

@ -87,6 +87,8 @@ pub struct UiState {
pub display_quality: designcraft_render::DisplayQuality,
/// Preferences › Interface › UI scaling (1 = 100%).
pub ui_scale: f32,
/// Preferences › Appearance of Black: 100% K on screen as rich black.
pub rich_black: bool,
/// Edit › Keyboard Shortcuts: command id → shortcut ("" = none), over the defaults.
pub shortcuts: std::collections::BTreeMap<String, String>,
pub control_bar: bool,
@ -138,6 +140,7 @@ impl Default for UiState {
hidden_characters: false,
display_quality: designcraft_render::DisplayQuality::High,
ui_scale: 1.0,
rich_black: false,
shortcuts: Default::default(),
control_bar: false,
tools_double_column: false,

View File

@ -22,6 +22,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("app.preferences", "Preferences…", Some("Cmd+K"), "{}"),
("window.uiScale", "UI Scaling", None, "{scale: 0.5–3 (1 = 100%)} — the size of the whole interface"),
("app.keyboardShortcuts", "Keyboard Shortcuts…", None, "{} — view and change the shortcut of any command"),
("window.richBlack", "Appearance of Black", None, "{on: bool} — show 100% black as rich black on screen (off: accurately)"),
("window.setShortcut", "Set Shortcut", None, "{id, shortcut: \"Cmd+Alt+J\" | \"\" (none) | null (default)} → {shortcut, conflicts: [ids]}"),
("window.resetShortcuts", "Reset Shortcuts", None, "{} — every command back to its default shortcut"),
("app.findChange", "Find/Change…", Some("Cmd+F"), "{}"),
@ -630,6 +631,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
designcraft_render::DisplayQuality::High => "high",
});
f["uiScale"] = json!((app.ui.ui_scale * 100.0).round());
f["richBlack"] = json!(app.ui.rich_black);
f["section"] = json!("general");
app.ui.dialog = Some(crate::dialogs::Dialog::new("preferences", f));
Ok(Value::Null)
@ -746,6 +748,11 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
app.ui.shortcuts.clear();
Ok(Value::Null)
}
"window.richBlack" => {
app.ui.rich_black = p.get("on").and_then(Value::as_bool).unwrap_or(!app.ui.rich_black);
app.canvas.shown = None;
Ok(json!({"on": app.ui.rich_black}))
}
"window.uiScale" => {
let v = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0) as f32;
app.ui.ui_scale = v.clamp(0.5, 3.0);

View File

@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 99 | 3 | 0 | 99% |
| P1 | 97 | 60 | 25 | 12 | 75% |
| P1 | 97 | 60 | 26 | 11 | 75% |
| P2 | 76 | 8 | 7 | 61 | 15% |
| Area | Parity |
@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Layers | 95% |
| Frames, shapes & paths | 87% |
| Transform | 93% |
| Fill, stroke, colour | 74% |
| Fill, stroke, colour | 76% |
| Effects & transparency | 50% |
| Placing & links | 78% |
| Type & text frames | 82% |
@ -157,7 +157,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
| Fill, stroke, colour | Overprint Preview | P1 | M |
| Fill, stroke, colour | Separations Preview | P2 | M |
| Fill, stroke, colour | Eyedropper | P1 | D |
| Fill, stroke, colour | Appearance of Black prefs | P1 | M |
| Fill, stroke, colour | Appearance of Black prefs | P1 | P |
| Effects & transparency | Opacity & blend modes | P0 | D |
| Effects & transparency | Isolate Blending / Knockout Group | P2 | M |
| Effects & transparency | Drop Shadow | P1 | D |