From bfe4220840ed0bfbbbef7fbcf8e23970e79d2fcc Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 2 Oct 2026 16:59:19 -0400 Subject: [PATCH] C: Appearance of Black on screen (rich black / accurate) Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/render/src/lib.rs | 38 +++++++++++++++++++++++++++++++++++ crates/ui-egui/src/canvas.rs | 2 ++ crates/ui-egui/src/dialogs.rs | 19 ++++++++++++++++++ crates/ui-egui/src/lib.rs | 3 +++ crates/ui-egui/src/menus.rs | 7 +++++++ docs/parity.md | 6 +++--- 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2db5077..150e878 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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). diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index fc78668..02c2bad 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -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 = 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()); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index c115d1a..42d68e8 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -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); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 81d0924..1bc5dc3 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -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 { _ => "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}))?; } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index be01ddb..222c202 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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, 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, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9f1680a..271c9b1 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -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 "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 { + 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); diff --git a/docs/parity.md b/docs/parity.md index f0f62eb..15541c1 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -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 |