From 3e5eeccfa2357b5341d0fdee35f3298bbbc66899 Mon Sep 17 00:00:00 2001 From: dfgodvan <180286157+dfgodvan@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:31:26 +0300 Subject: [PATCH] Render: fade dimmed headers and footers as one layer While the body is edited, headers and footers are drawn dimmed. Each draw item got the dimming alpha on its own, so wherever two of them overlap the alpha was applied twice: a header table's borders came out darker where they meet and along shared cell edges, nearly black at crossings. The PDF export and the undimmed view were fine. page_layers gives a page's draws as two layers (headers/footers, body) at full strength with the opacity to draw each at. The canvas draws a dimmed layer inside one opacity layer, as Word dims it, and background_at blends it the same way. page_display is unchanged for the PDF export and tests. --- crates/layout/src/display.rs | 45 ++++++++++++++++---- crates/render/src/lib.rs | 56 ++++++++++++++++++++++--- crates/render/src/tests_color_glyphs.rs | 1 + 3 files changed, 87 insertions(+), 15 deletions(-) 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.