diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index ac185780..09338405 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -207,7 +207,9 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h #[allow(clippy::too_many_arguments)] fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h: f64, boxes: Option<&crate::chartex::Boxes>) { - let text_col = c.text_color.as_ref().map(|t| rctx.color(t, None)).unwrap_or(Rgba::rgb(0x59, 0x59, 0x59)); + // Text without a colour of its own takes the theme's text colour, so a chart on a dark + // theme is readable (a fixed grey used to vanish there). + let text_col = rctx.color(c.text_color.as_ref().unwrap_or(&ColorRef::scheme(SchemeSlot::Tx1)), None); let grid_col = Rgba::rgb(0xD9, 0xD9, 0xD9); if let Some(f) = &c.chart_fill { crate::paint::fill_path( diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index b65098b0..baccc9dd 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -2717,3 +2717,33 @@ fn empty_bullet_is_gray_only_while_editing_and_never_in_exports() { assert!(changed.iter().any(|(px, _)| px[0] == 179 && px[1] == 179 && px[2] == 179)); assert!(changed.iter().all(|(px, _)| px[0] >= 179 && px[0] == px[1] && px[1] == px[2])); } + +/// A chart that has no text colour of its own takes the theme's: light on Nocturne's dark slides +/// (it used to be a fixed grey, near-invisible there), dark on Harbor's light ones. +#[test] +fn chart_text_follows_the_theme_when_it_has_no_colour_of_its_own() { + let draw = |theme: &str, title: Option<&str>| { + let mut c = Chart::sample(ChartType::Column); + c.title = title.map(String::from); + c.legend = None; + c.data_labels = false; + let sh = Shape { id: ShapeId(630), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }; + let mut p = deck_with(vec![sh]); + let t = deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name == theme).expect("theme"); + std::sync::Arc::make_mut(&mut p.masters[0]).theme = t; + render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }) + }; + let lum = |px: [u8; 4]| (px[0] as u32 * 299 + px[1] as u32 * 587 + px[2] as u32 * 114) / 1000; + // Pixels in the title band that pass `f`. + let count = |img: &Image, f: &dyn Fn(u32) -> bool| { + (200..760u32).flat_map(|x| (4..70u32).map(move |y| (x, y))).filter(|(x, y)| f(lum(img.pixel(*x, *y)))).count() + }; + let (dark_bare, dark_titled) = (draw("Nocturne", None), draw("Nocturne", Some("Quarterly revenue"))); + assert!(lum(dark_bare.pixel(5, 5)) < 60, "Nocturne's slide is dark: {:?}", dark_bare.pixel(5, 5)); + let light_ink = count(&dark_titled, &|l| l > 200) as i64 - count(&dark_bare, &|l| l > 200) as i64; + assert!(light_ink > 100, "the title is light on a dark theme (light pixels added: {light_ink})"); + let (light_bare, light_titled) = (draw("Harbor", None), draw("Harbor", Some("Quarterly revenue"))); + assert!(lum(light_bare.pixel(5, 5)) > 200, "Harbor's slide is light: {:?}", light_bare.pixel(5, 5)); + let dark_ink = count(&light_titled, &|l| l < 80) as i64 - count(&light_bare, &|l| l < 80) as i64; + assert!(dark_ink > 100, "the title is dark on a light theme (dark pixels added: {dark_ink})"); +}