Merge pull request #1886 from sbjwin/fix/thumbnail-grayscale

UI: follow Grayscale view mode in slide thumbnails
This commit is contained in:
FredApps
2026-11-05 05:31:42 +01:00
committed by GitHub
5 changed files with 38 additions and 11 deletions
+2 -1
View File
@@ -207,7 +207,8 @@ fn preview(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage, doc: &deckcraft_
let mut budget = 4u32;
let mut draw_slide = |ui: &mut Ui, i: usize, r: Rect| {
let px = (r.width() * ui.ctx().pixels_per_point()).round().clamp(16.0, 1600.0) as u32;
match app.textures.thumb(ui.ctx(), doc, i, px, &mut budget) {
// Print preview matches the actual exported PDF output (always full color).
match app.textures.thumb(ui.ctx(), doc, i, px, false, &mut budget) {
Some(tex) => {
ui.painter().image(tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
+1 -1
View File
@@ -299,7 +299,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
vec2(w as f32 * scale, h as f32 * scale),
);
let mut budget = 2;
if let Some(tex) = app.textures.thumb(ui.ctx(), &doc, idx, (r.width() * ppp).round().max(1.0) as u32, &mut budget) {
if let Some(tex) = app.textures.thumb(ui.ctx(), &doc, idx, (r.width() * ppp).round().max(1.0) as u32, app.ui.grayscale, &mut budget) {
painter.image(tex.id(), r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
}
+1 -1
View File
@@ -219,7 +219,7 @@ fn list(app: &mut SlideApp, ui: &mut Ui) {
// Only rows in view (plus a little margin) ask for a thumbnail: with every row asking, a
// deck bigger than the thumbnail cache evicts its own thumbnails and never settles (#969).
let near = ui.clip_rect().expand2(vec2(0.0, 2.0 * row.height())).intersects(row);
let tex = if near { app.textures.thumb(ui.ctx(), &doc, i, wpx, &mut budget) } else { None };
let tex = if near { app.textures.thumb(ui.ctx(), &doc, i, wpx, app.ui.grayscale, &mut budget) } else { None };
match tex {
Some(tex) => {
p.image(
+3 -2
View File
@@ -209,7 +209,8 @@ fn place_slides(app: &mut SlideApp, ui: &mut Ui, g: &Grid<'_>, budget: &mut u32,
// Only cells in view (plus a little margin) ask for a thumbnail, so a deck bigger
// than the thumbnail cache does not evict its own thumbnails forever (#969).
let near = ui.clip_rect().expand2(vec2(0.0, 2.0 * cell.height())).intersects(cell);
let tex = if near { app.textures.thumb(ui.ctx(), g.doc, i, (img.width() * ppp).round() as u32, budget) } else { None };
let tex =
if near { app.textures.thumb(ui.ctx(), g.doc, i, (img.width() * ppp).round() as u32, app.ui.grayscale, budget) } else { None };
match tex {
Some(tex) => {
p.image(tex.id(), img, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
@@ -391,7 +392,7 @@ pub fn notes_page(app: &mut SlideApp, ui: &mut Ui) {
}
let ppp = ui.ctx().pixels_per_point();
let mut budget = 2;
if let Some(tex) = app.textures.thumb(ui.ctx(), &doc, idx, (srect.width() * ppp).round() as u32, &mut budget) {
if let Some(tex) = app.textures.thumb(ui.ctx(), &doc, idx, (srect.width() * ppp).round() as u32, app.ui.grayscale, &mut budget) {
ui.painter().image(tex.id(), srect, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
ui.painter().rect_stroke(srect, CornerRadius::ZERO, Stroke::new(1.0, Color32::GRAY), egui::StrokeKind::Outside);
+31 -6
View File
@@ -361,9 +361,19 @@ impl Textures {
self.thumbs.clear();
}
/// A thumbnail texture; renders at most `budget` new thumbnails per frame (returns None when
/// it is over budget, so the caller draws a placeholder and asks for another frame).
pub fn thumb(&mut self, ctx: &egui::Context, p: &Presentation, index: usize, width_px: u32, budget: &mut u32) -> Option<TextureHandle> {
/// A thumbnail texture; renders at most `budget` new thumbnails per frame. Over budget it
/// returns the slide's previous thumbnail if there is one (and asks for another frame), else
/// None, so the caller draws a placeholder.
/// When `grayscale` is true, the thumbnail renders in black-and-white (matching Grayscale view).
pub fn thumb(
&mut self,
ctx: &egui::Context,
p: &Presentation,
index: usize,
width_px: u32,
grayscale: bool,
budget: &mut u32,
) -> Option<TextureHandle> {
let slide = p.slides.get(index)?.clone();
let master = master_of(p, &slide);
let h = (width_px as f64 * p.slide_size.height / p.slide_size.width.max(1.0)).round().max(1.0) as u32;
@@ -372,7 +382,7 @@ impl Textures {
master: master.as_ref().map(|m| Arc::as_ptr(m) as usize).unwrap_or(0),
size: (width_px, h),
edit: false,
extra: 0,
extra: grayscale as u64,
want: Region::slide(width_px, h),
editing_text: None,
};
@@ -385,7 +395,8 @@ impl Textures {
return Some(e.tex.clone());
}
if *budget == 0 {
// Keep showing the stale thumbnail while the new one waits.
// Keep showing the stale thumbnail while the new one waits; request a repaint to finish rendering.
ctx.request_repaint();
return self.thumbs.get(&key.slide).map(|e| e.tex.clone());
}
*budget -= 1;
@@ -393,7 +404,7 @@ impl Textures {
let scale = tw as f64 / p.slide_size.width.max(1.0);
let img = deckcraft_render::render_slide(p, index, &RenderOpts { scale, size: Some((tw, th)), threads: 0, ..Default::default() });
self.thumb_renders += 1;
let tex = upload_editor(ctx, format!("thumb-{index}"), to_color_image(&img, false));
let tex = upload_editor(ctx, format!("thumb-{index}"), to_color_image(&img, grayscale));
// Drop the old entry for this slide address; entries for slides that no longer exist age out.
let region = key.want;
self.thumbs.insert(key.slide, Entry { key, tex: tex.clone(), region, _slide: slide, _master: master, stamp: clock });
@@ -433,6 +444,20 @@ mod tests {
assert_eq!(device_pixel_rect(aligned, ppp), aligned);
}
#[test]
fn to_color_image_turns_colors_to_grayscale_when_flag_is_set() {
let img = deckcraft_render::Image { width: 2, height: 1, pixels: vec![0, 0, 255, 255, 255, 0, 0, 255] };
let color = super::to_color_image(&img, false);
assert_eq!(color.pixels[0], egui::Color32::from_rgba_premultiplied(0, 0, 255, 255));
assert_eq!(color.pixels[1], egui::Color32::from_rgba_premultiplied(255, 0, 0, 255));
let gray = super::to_color_image(&img, true);
assert_eq!(gray.pixels[0].r(), gray.pixels[0].g());
assert_eq!(gray.pixels[0].g(), gray.pixels[0].b());
assert_eq!(gray.pixels[1].r(), gray.pixels[1].g());
assert_eq!(gray.pixels[1].g(), gray.pixels[1].b());
}
#[test]
fn a_bad_scale_does_not_move_the_rect() {
let r = Rect::from_min_size(pos2(3.0, 4.0), vec2(10.0, 12.0));