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.
This commit is contained in:
dfgodvan
2026-10-19 14:05:15 +03:00
parent 117aea31a8
commit 3e5eeccfa2
3 changed files with 87 additions and 15 deletions
+36 -9
View File
@@ -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<Draw> {
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<Draw>); 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<Draw>) {
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<Placed>], opts: &DisplayOptions, a: f32, out: &mut Vec<Draw>) {
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<u32> = Vec::new();
@@ -439,17 +470,13 @@ pub fn page_display(doc: &Document, page: &Page, opts: &DisplayOptions) -> Vec<D
if let Some((_, rect, warp)) = warps.iter().find(|(w, ..)| w == id) {
let lines: Vec<&Placed> =
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
+50 -6
View File
@@ -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);
+1
View File
@@ -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.