diff --git a/crates/layout/src/display.rs b/crates/layout/src/display.rs index 10fc9faa2..7233bb7b3 100644 --- a/crates/layout/src/display.rs +++ b/crates/layout/src/display.rs @@ -409,10 +409,41 @@ fn stroke_of(u: Underline) -> Stroke { /// Draw items for a page. pub fn page_display(doc: &Document, page: &Page, opts: &DisplayOptions) -> Vec { let mut out = Vec::new(); + let (ha, ba) = dimming(opts); + stories_display(doc, &[&page.header, &page.footer], opts, ha, &mut out); + stories_display(doc, &[&page.items], opts, ba, &mut out); + body_change_bars(doc, page, opts, ba, &mut out); + out +} + +/// A page's draw items as two layers, its headers and footers then its body, each at full +/// strength with the opacity to draw it at. A dimmed layer is faded as one picture, as Word dims +/// it: where its lines or fills overlap (a table's borders meeting) they don't darken each other. +pub fn page_layers(doc: &Document, page: &Page, opts: &DisplayOptions) -> [(f32, Vec); 2] { + let (ha, ba) = dimming(opts); + let (mut hf, mut body) = (Vec::new(), Vec::new()); + stories_display(doc, &[&page.header, &page.footer], opts, 1.0, &mut hf); + stories_display(doc, &[&page.items], opts, 1.0, &mut body); + body_change_bars(doc, page, opts, 1.0, &mut body); + [(ha, hf), (ba, body)] +} + +/// The opacity of the headers/footers and of the body. +fn dimming(opts: &DisplayOptions) -> (f32, f32) { + let a = |dim: bool| if dim { 0.45 } else { 1.0 }; + (a(opts.dim_header), a(opts.dim_body)) +} + +fn body_change_bars(doc: &Document, page: &Page, opts: &DisplayOptions, alpha: f32, out: &mut Vec) { + if opts.markup && (opts.revisions.insertions_deletions || opts.revisions.formatting) && opts.revisions.changed_lines != ChangeBar::None { + change_bars(doc, page, &opts.revisions, alpha, out); + } +} + +/// Draw items for the placed items of `stories`, faded by `a`. +fn stories_display(doc: &Document, stories: &[&Vec], opts: &DisplayOptions, a: f32, out: &mut Vec) { let hidden = opts.hide_object.as_ref().map(|p| Hidden::new(doc, p)); - let ha = if opts.dim_header { 0.45 } else { 1.0 }; - let ba = if opts.dim_body { 0.45 } else { 1.0 }; - for (items, a) in [(&page.header, ha), (&page.footer, ha), (&page.items, ba)] { + for items in stories { let warps = warped_boxes(doc, items); let clips = clipped_cells(items); let mut done: Vec = Vec::new(); @@ -439,17 +470,13 @@ pub fn page_display(doc: &Document, page: &Page, opts: &DisplayOptions) -> Vec = items.iter().filter(|i| matches!(i, Placed::Lines { story: StoryRef::Part(s), .. } if s == id)).collect(); - warped_text(doc, &lines, *rect, warp, opts, a, &mut out); + warped_text(doc, &lines, *rect, warp, opts, a, out); } } - _ => item(doc, it, opts, a, &mut out), + _ => item(doc, it, opts, a, out), } } } - if opts.markup && (opts.revisions.insertions_deletions || opts.revisions.formatting) && opts.revisions.changed_lines != ChangeBar::None { - change_bars(doc, page, &opts.revisions, ba, &mut out); - } - out } /// One object drawn alone, as its page draws it: the picture, shape or group `o` in `rect` (its diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index b0416c11e..01e70c54a 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -12,7 +12,7 @@ use wordcraft_doc::para::ShapeKind; use wordcraft_doc::props::Rgb; use wordcraft_fonts::FontDb; use wordcraft_layout::Page; -use wordcraft_layout::display::{DisplayOptions, Draw, Stroke, page_display}; +use wordcraft_layout::display::{DisplayOptions, Draw, Stroke, page_layers}; use wordcraft_layout::wordart::ArtPaint; use wordcraft_metafile::{Item, Picture, PlacedItem, Seg}; @@ -524,13 +524,22 @@ fn render_region_capped(doc: &Document, page: &Page, w: u32, h: u32, view: Affin ctx.set_paint(color(opts.ink(doc.settings.page_color.unwrap_or(opts.paper)), 1.0)); ctx.fill_rect(&kurbo::Rect::new(0.0, 0.0, w16 as f64, h16 as f64)); draw_page_picture(&mut ctx, doc, page, view); - let items = page_display(doc, page, &opts.display); let visible = view.inverse().transform_rect_bbox(kurbo::Rect::new(0.0, 0.0, w16 as f64, h16 as f64)).inflate(40.0, 40.0); if let Some(wm) = &doc.settings.watermark { draw_watermark(&mut ctx, view, page, wm, opts); } - for it in &items { - draw(&mut ctx, doc, it, view, &visible, opts); + // A dimmed layer is drawn whole, then faded once, so its overlaps don't darken. + for (alpha, items) in page_layers(doc, page, &opts.display) { + let faded = alpha < 1.0 && !items.is_empty(); + if faded { + ctx.push_opacity_layer(alpha); + } + for it in &items { + draw(&mut ctx, doc, it, view, &visible, opts); + } + if faded { + ctx.pop_layer(); + } } ctx.flush(); let mut pixels = vec![0u8; w16 as usize * h16 as usize * 4]; @@ -551,8 +560,14 @@ pub fn background_at(doc: &Document, page: &Page, opts: &RenderOptions, x: f32, return paper; } let mut bg = [paper.0 as f32, paper.1 as f32, paper.2 as f32]; - let items = page_display(doc, page, &opts.display); - blend_backgrounds(&items, wordcraft_geom::Point::new(x, y), opts, &mut bg, 0); + for (alpha, items) in page_layers(doc, page, &opts.display) { + // A faded layer: its own colour there, blended in at its opacity. + let mut layer = bg; + blend_backgrounds(&items, wordcraft_geom::Point::new(x, y), opts, &mut layer, 0); + for (v, n) in bg.iter_mut().zip(layer) { + *v += (n - *v) * alpha; + } + } let c = |v: f32| v.round().clamp(0.0, 255.0) as u8; Rgb(c(bg[0]), c(bg[1]), c(bg[2])) } @@ -1187,6 +1202,7 @@ pub fn render_items(doc: &Document, items: &[Draw], x: f32, y: f32, w: f32, h: f #[cfg(test)] mod tests { use super::*; + use wordcraft_layout::display::page_display; use wordcraft_layout::{LayoutCache, LayoutOptions, layout}; /// The first and last pixel rows with ink in a black-on-white [`render_text_line`] raster. @@ -1554,6 +1570,34 @@ mod tests { assert!(!ink(&img, 31, 3), "the upper strand ends at x=30"); } + /// A dimmed header is its full-strength picture faded once, as Word dims it: where a header + /// table's borders meet they are the tone of the borders, not darker. + #[test] + fn dimmed_header_borders_dont_darken_where_they_meet() { + let mut d = Document::from_text(""); + let t = wordcraft_doc::Table::new(2, 2, 200.0); + let blocks = vec![std::sync::Arc::new(wordcraft_doc::Block::Table(t)), wordcraft_doc::para_block(wordcraft_doc::Paragraph::new())]; + let id = d.add_part(wordcraft_doc::PartKind::Header, blocks); + d.last_section.headers.default = Some(id); + let l = layout(&d, &mut LayoutCache::new(), &LayoutOptions::default()); + let render = |dim_header: bool| { + let mut opts = RenderOptions::default(); + opts.display.dim_header = dim_header; + render_page(&d, &l.pages[0], 3.0, &opts) + }; + let (full, dim) = (render(false), render(true)); + let mut ink = 0; + for y in 0..full.height { + for x in 0..full.width { + let (f, g) = (full.pixel(x, y)[0] as f32, dim.pixel(x, y)[0] as f32); + ink += usize::from(f < 128.0); + let want = 255.0 - 0.45 * (255.0 - f); + assert!((g - want).abs() <= 2.0, "({x}, {y}): {g} dimmed, {want} wanted ({f} at full strength)"); + } + } + assert!(ink > 100, "the borders are drawn: {ink}"); + } + #[test] fn dark_page_inverts_lightness() { assert_eq!(invert_lightness(Rgb::WHITE), Rgb::BLACK); diff --git a/crates/render/src/tests_color_glyphs.rs b/crates/render/src/tests_color_glyphs.rs index 531f2e6ab..c1d38a7a8 100644 --- a/crates/render/src/tests_color_glyphs.rs +++ b/crates/render/src/tests_color_glyphs.rs @@ -1,6 +1,7 @@ //! Colour glyphs (#1269): emoji from colour fonts (bitmap strikes, COLR) are drawn in colour. use super::*; +use wordcraft_layout::display::page_display; use wordcraft_layout::{LayoutCache, LayoutOptions, layout}; /// Colour emoji families the system fallback may pick.