mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 20:47:32 -06:00
fix(ui): Update previews the page without the marks it replaces
Update Background, Update Watermark and Update Header and Footer previewed the new marks on the page's thumbnail, which still has the marks Update replaces. A pale yellow background updated to light blue showed a muddy teal (the blue multiplied onto the yellow), and a new watermark or header was drawn over the old one. The applied result was right; only the preview was wrong. In Update, the preview now draws the page without its marks of that kind. The engine removes them on a copy of the document (Document::page_without_marks, read-only like the other preview layers), and the dialog renders that page off the UI thread at the preview's size, showing a blank sheet until it arrives. Add, and pages without marks of that kind, keep the thumbnail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
833cd63c8a
commit
2b83ae6a7a
@@ -2999,6 +2999,42 @@ fn headers_footers_watermarks_and_backgrounds_show_update_and_remove() {
|
||||
assert_eq!(s.get(id).unwrap().can_undo(), Some("Remove background"));
|
||||
}
|
||||
|
||||
/// Update previews its marks on the page without the ones it replaces: a yellow background
|
||||
/// updated to blue isn't previewed as blue over yellow, nor a new watermark over the old one.
|
||||
/// The other kinds of mark and the page's own content stay, a page without that kind has nothing
|
||||
/// to prepare, and preparing changes nothing in the document.
|
||||
#[test]
|
||||
fn update_previews_on_the_page_without_the_marks_it_replaces() {
|
||||
let (mut s, id) = session_with(2);
|
||||
let yellow = Background { color: [1.0, 1.0, 0.0], ..Background::default() };
|
||||
s.apply(id, Edit::AddBackground { pages: vec![0], settings: yellow, replace: false, file: None }).unwrap();
|
||||
let red = Watermark { text: "SECRET".into(), color: [1.0, 0.0, 0.0], opacity: 1.0, ..Watermark::default() };
|
||||
s.apply(id, Edit::AddWatermark { pages: vec![0], settings: red, replace: false, file: None }).unwrap();
|
||||
let doc = s.get(id).unwrap();
|
||||
let (generation, dirty) = (doc.edit_generation(), doc.dirty);
|
||||
let px = |p: &pdfcraft_render::RenderedPage, x: u32, y: u32| -> [u8; 3] {
|
||||
p.rgba[(((300 - y) * p.width + x) * 4) as usize..][..3].try_into().unwrap()
|
||||
};
|
||||
let reds = |p: &pdfcraft_render::RenderedPage| p.rgba.as_chunks::<4>().0.iter().filter(|[r, g, b, _]| *r > 200 && *g < 80 && *b < 80).count();
|
||||
let dark = |p: &pdfcraft_render::RenderedPage| p.rgba.as_chunks::<4>().0.iter().filter(|[r, g, b, _]| *r < 100 && *g < 100 && *b < 100).count();
|
||||
assert_eq!(pixel(&s, id, 0, 5, 5)[..3], [255, 255, 0], "the page has the yellow background");
|
||||
|
||||
let page = doc.page_without_marks(0, MarkKind::Background).unwrap().expect("page 1 has a background").render_background(1.0).unwrap();
|
||||
assert_eq!(px(&page, 5, 5), [255, 255, 255], "without the background the page is white");
|
||||
assert!(reds(&page) > 50, "the watermark stays");
|
||||
assert!(dark(&page) > 10, "the page text stays");
|
||||
|
||||
let page = doc.page_without_marks(0, MarkKind::Watermark).unwrap().expect("page 1 has a watermark").render_background(1.0).unwrap();
|
||||
assert_eq!(reds(&page), 0, "without the watermark");
|
||||
assert_eq!(px(&page, 5, 5), [255, 255, 0], "the background stays");
|
||||
|
||||
assert!(doc.page_without_marks(1, MarkKind::Background).unwrap().is_none(), "page 2 has none: its thumbnail is the page");
|
||||
assert!(doc.page_without_marks(0, MarkKind::HeaderFooter).unwrap().is_none(), "the document has no header");
|
||||
assert!(doc.page_without_marks(9, MarkKind::Background).is_err(), "no page 10");
|
||||
let doc = s.get(id).unwrap();
|
||||
assert_eq!((doc.edit_generation(), doc.dirty, doc.can_undo()), (generation, dirty, Some("Add watermark")), "the document is unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marker_indexes_match_after_open_and_save_when_requested() {
|
||||
let (mut source, source_id) = session_with(1);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! The page beneath an inline text editor, with the source glyphs removed on a COS clone.
|
||||
//! The page beneath an inline text editor, with the source glyphs removed on a COS clone; and,
|
||||
//! the same way, the page beneath a dragged added item or the marks an Update replaces.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -90,6 +91,24 @@ impl Document {
|
||||
})?
|
||||
}
|
||||
|
||||
/// The page without its marks of `kind`: what Update replaces them on, so its preview doesn't
|
||||
/// draw the new marks over the old ones (or tint the old background). `None` when the page has
|
||||
/// none, and its thumbnail already is that page. Read-only, like [`Self::text_edit_preview`].
|
||||
pub fn page_without_marks(&self, page: usize, kind: crate::MarkKind) -> Result<Option<TextEditPreview>, String> {
|
||||
if !self.marks().contains(&kind) {
|
||||
return Ok(None);
|
||||
}
|
||||
guard(|| {
|
||||
let editor = self.editor.as_ref().ok_or("the document can't be read")?;
|
||||
let mut cos = editor.cos.clone();
|
||||
if pdfcraft_edit::remove_marks(&mut cos, &[page], kind).map_err(|e| e.to_string())? == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let bytes = Arc::new(pdfcraft_cos::write_incremental(&cos, &pdfcraft_cos::SaveOptions::default()).map_err(|e| e.to_string())?);
|
||||
Ok(Some(TextEditPreview { page, bytes, config: self.config.clone() }))
|
||||
})?
|
||||
}
|
||||
|
||||
/// An added item alone, on a transparent page: what follows the pointer while it is dragged,
|
||||
/// so the page under its first place doesn't come along. Read-only, like
|
||||
/// [`Self::added_content_preview`].
|
||||
|
||||
@@ -1586,6 +1586,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
app.view_draft = None;
|
||||
app.custom_draft = None;
|
||||
app.bulk_field_props = None;
|
||||
app.unmarked_page = Default::default();
|
||||
} else {
|
||||
app.dialog = Some(next);
|
||||
}
|
||||
|
||||
@@ -955,6 +955,8 @@ pub struct PdfCraftApp {
|
||||
pub boxes_draft: pageboxes::BoxesDraft,
|
||||
/// Header & footer / watermark / background dialog state.
|
||||
pub marks_draft: marks_ui::MarksDraft,
|
||||
/// The Update dialogs' preview page without the marks they replace.
|
||||
pub(crate) unmarked_page: marks_ui::UnmarkedPage,
|
||||
/// Export dialog settings.
|
||||
pub export_draft: export_ui::ExportDraft,
|
||||
/// A running export's progress.
|
||||
@@ -1279,6 +1281,7 @@ impl PdfCraftApp {
|
||||
protect_draft: Default::default(),
|
||||
boxes_draft: Default::default(),
|
||||
marks_draft: Default::default(),
|
||||
unmarked_page: Default::default(),
|
||||
export_draft: Default::default(),
|
||||
export_status: None,
|
||||
signature: None,
|
||||
@@ -2878,7 +2881,7 @@ impl PdfCraftApp {
|
||||
|
||||
/// `true` while any open document still waits for page renders (used by headless capture).
|
||||
pub fn render_pending(&self) -> bool {
|
||||
self.views.iter().any(|v| v.render_pending())
|
||||
self.views.iter().any(|v| v.render_pending()) || self.unmarked_page.pending()
|
||||
}
|
||||
|
||||
/// Apply a named view option (`--page 3`, `--panel bookmarks`, `--theme dark`, …).
|
||||
|
||||
@@ -12,6 +12,9 @@ use crate::{PdfCraftApp, icons, widgets};
|
||||
/// How far a watermark may be moved from the page centre, in points.
|
||||
const MAX_OFFSET: f64 = 20_000.0;
|
||||
|
||||
/// The preview page's longer side, in points.
|
||||
const PREVIEW_SIDE: f32 = 190.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Subset {
|
||||
All,
|
||||
@@ -152,7 +155,7 @@ fn preview(
|
||||
draw: impl FnOnce(&egui::Painter, Rect, f32),
|
||||
) {
|
||||
let (w, h) = (size.0.max(1.0), size.1.max(1.0));
|
||||
let s = (190.0 / w.max(h)) as f32;
|
||||
let s = (f64::from(PREVIEW_SIDE) / w.max(h)) as f32;
|
||||
let (r, _) = ui.allocate_exact_size(vec2(210.0, 210.0), egui::Sense::hover());
|
||||
let page = Rect::from_center_size(r.center(), vec2(w as f32 * s, h as f32 * s));
|
||||
let painter = ui.painter_at(r);
|
||||
@@ -180,6 +183,69 @@ fn behind(color: [f64; 3], opacity: f64) -> Color32 {
|
||||
rgb32(color.map(|c| 1.0 - a * (1.0 - c)))
|
||||
}
|
||||
|
||||
/// In Update, the page previewed without the marks the dialog replaces. Its thumbnail still has
|
||||
/// them, so the new marks would be drawn over the old ones, and a new background colour would tint
|
||||
/// the old one. Rendered off the UI thread like the thumbnail, once per page shown.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct UnmarkedPage {
|
||||
/// The document, its edit generation, the page and the kind of mark taken away.
|
||||
key: Option<(pdfcraft_engine::DocId, u64, usize, MarkKind)>,
|
||||
renderer: Option<pdfcraft_render::RenderPool>,
|
||||
texture: Option<egui::TextureHandle>,
|
||||
tag: Option<u64>,
|
||||
}
|
||||
|
||||
impl UnmarkedPage {
|
||||
pub(crate) fn pending(&self) -> bool {
|
||||
self.renderer.is_some() && self.texture.is_none()
|
||||
}
|
||||
|
||||
/// The picture to preview `page` on in place of its thumbnail: `None` when the thumbnail is
|
||||
/// that page (not Update, or no marks of `kind` on it, or the page can't be prepared), and
|
||||
/// `Some(None)` while it renders, when the page is a blank sheet.
|
||||
fn picture(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
(doc, id): (&pdfcraft_engine::Document, pdfcraft_engine::DocId),
|
||||
page: usize,
|
||||
kind: MarkKind,
|
||||
replace: bool,
|
||||
) -> Option<Option<egui::TextureId>> {
|
||||
let key = replace.then(|| (id, doc.edit_generation(), page, kind));
|
||||
if self.key != key {
|
||||
*self = Self { key, ..Default::default() };
|
||||
if replace {
|
||||
self.renderer = doc.page_without_marks(page, kind).ok().flatten().map(|p| p.renderer());
|
||||
}
|
||||
}
|
||||
let renderer = self.renderer.as_ref()?;
|
||||
// At the preview's size in pixels.
|
||||
let p = doc.info.pages.get(page)?;
|
||||
let scale = PREVIEW_SIDE * ctx.pixels_per_point() / p.width.max(p.height).max(1.0);
|
||||
let tag = (scale * 1000.0) as u64;
|
||||
if self.tag != Some(tag) {
|
||||
renderer.set_queue(vec![pdfcraft_render::RenderRequest { page, scale, tag, ..Default::default() }]);
|
||||
self.tag = Some(tag);
|
||||
}
|
||||
if let Some(out) = renderer.try_recv() {
|
||||
if out.error.is_some() {
|
||||
// The thumbnail, as before, rather than no page at all.
|
||||
self.renderer = None;
|
||||
self.texture = None;
|
||||
return None;
|
||||
}
|
||||
if out.request.tag == tag {
|
||||
let image = crate::canvas::texture_image([out.width as usize, out.height as usize], out.rgba);
|
||||
self.texture = Some(ctx.load_texture("marks-preview-unmarked", image, egui::TextureOptions::LINEAR));
|
||||
}
|
||||
}
|
||||
if self.pending() {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(16));
|
||||
}
|
||||
Some(self.texture.as_ref().map(|t| t.id()))
|
||||
}
|
||||
}
|
||||
|
||||
/// The page (0-based) the preview shows: the current one when the range selects it, otherwise the
|
||||
/// first page the range selects (the current one again when it selects none).
|
||||
fn preview_page(range: &PageRange, current: usize, count: usize) -> usize {
|
||||
@@ -378,12 +444,15 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
||||
ui.add_space(8.0);
|
||||
// The preview: the current page (or the page stepped to), or the first one the range selects
|
||||
// if it leaves that out, drawn from its thumbnail at its displayed size (after /Rotate), the
|
||||
// frame the engine lays the marks out in.
|
||||
// frame the engine lays the marks out in. Update draws it without the marks it replaces.
|
||||
let target = shown_preview(&d.range, &mut d.preview, current, count);
|
||||
let page = doc.info.pages.get(target).map(|p| (p.width as f64, p.height as f64)).unwrap_or((612.0, 792.0));
|
||||
let picture = app.views.get_mut(i).and_then(|view| {
|
||||
view.need_thumbnail(target, true);
|
||||
view.thumb_id(target)
|
||||
let unmarked = app.unmarked_page.picture(ui.ctx(), (doc, id), target, kind, d.replace);
|
||||
let picture = unmarked.unwrap_or_else(|| {
|
||||
app.views.get_mut(i).and_then(|view| {
|
||||
view.need_thumbnail(target, true);
|
||||
view.thumb_id(target)
|
||||
})
|
||||
});
|
||||
let tint = if kind == MarkKind::Background && !d.use_file { behind(d.bg.color, d.bg.opacity) } else { Color32::WHITE };
|
||||
let hf = d.hf.clone();
|
||||
|
||||
@@ -294,6 +294,159 @@ fn the_preview_steps_through_the_pages_and_starts_at_the_current_one() {
|
||||
h.get_by_label("Preview: page 2 of 3");
|
||||
}
|
||||
|
||||
/// Keeps every texture the app uploads, so a test can read what a picture it paints shows.
|
||||
#[derive(Clone, Default)]
|
||||
struct Uploads(std::sync::Arc<std::sync::Mutex<std::collections::HashMap<egui::TextureId, std::sync::Arc<egui::ColorImage>>>>);
|
||||
|
||||
impl egui_kittest::TestRenderer for Uploads {
|
||||
fn handle_delta(&mut self, delta: &mut egui::TexturesDelta) {
|
||||
let mut seen = self.0.lock().unwrap();
|
||||
for (id, images) in delta.set.drain() {
|
||||
for d in images.into_iter().filter(|d| d.pos.is_none()) {
|
||||
let egui::ImageData::Color(image) = d.image;
|
||||
seen.insert(id, image);
|
||||
}
|
||||
}
|
||||
// Freed textures stay readable: ids are never reused.
|
||||
delta.free.clear();
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &egui::Context, _: &egui::FullOutput) -> Result<image::RgbaImage, String> {
|
||||
Err("this renderer only keeps the uploads".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The page picture the dialog's preview painted last frame, left of its "Preview: page …" label,
|
||||
/// once its texture is in: its tint and its image.
|
||||
fn preview_picture(h: &Harness<'static, PdfCraftApp>, uploads: &Uploads) -> Option<(egui::Color32, std::sync::Arc<egui::ColorImage>)> {
|
||||
let label = h.query_by_label_contains("Preview: page")?.rect();
|
||||
h.output().shapes.iter().find_map(|s| {
|
||||
let egui::Shape::Mesh(m) = &s.shape else { return None };
|
||||
let r = m.calc_bounds();
|
||||
// The size of the 210-point box left of the label (which is centred down its side), and
|
||||
// not the stepper's arrow beside the label.
|
||||
let beside = r.width() > 50.0
|
||||
&& r.height() <= 215.0
|
||||
&& r.right() <= label.left()
|
||||
&& label.left() - r.right() < 260.0
|
||||
&& r.top() <= label.bottom()
|
||||
&& r.bottom() >= label.top();
|
||||
if !beside {
|
||||
return None;
|
||||
}
|
||||
let image = uploads.0.lock().unwrap().get(&m.texture_id).cloned()?;
|
||||
Some((m.vertices.first()?.color, image))
|
||||
})
|
||||
}
|
||||
|
||||
/// [`preview_picture`], waiting until `ready` accepts it (the page renders off the UI thread).
|
||||
fn wait_for_preview(
|
||||
h: &mut Harness<'static, PdfCraftApp>,
|
||||
uploads: &Uploads,
|
||||
what: &str,
|
||||
ready: impl Fn(&egui::ColorImage) -> bool,
|
||||
) -> (egui::Color32, std::sync::Arc<egui::ColorImage>) {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
loop {
|
||||
h.step();
|
||||
let shown = preview_picture(h, uploads);
|
||||
if let Some((tint, image)) = shown.clone().filter(|(_, image)| ready(image)) {
|
||||
return (tint, image);
|
||||
}
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"the preview never showed {what}; last (tint, size, middle of the lower half): {:?}",
|
||||
shown.map(|(tint, i)| (tint, i.size, at(&i, 0.5, 0.75)))
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
|
||||
/// The pixel of `image` at (u, v), from 0 to 1 across and down.
|
||||
fn at(image: &egui::ColorImage, u: f32, v: f32) -> egui::Color32 {
|
||||
let [w, h] = image.size;
|
||||
let (x, y) = (((u * w as f32) as usize).min(w - 1), ((v * h as f32) as usize).min(h - 1));
|
||||
image.pixels[y * w + x]
|
||||
}
|
||||
|
||||
fn count(image: &egui::ColorImage, f: impl Fn(egui::Color32) -> bool) -> usize {
|
||||
image.pixels.iter().filter(|&&c| f(c)).count()
|
||||
}
|
||||
|
||||
/// Update draws its preview on the page without the marks it replaces, which the page's thumbnail
|
||||
/// still has: a pale yellow background updated to light blue previews light blue, not blue
|
||||
/// multiplied onto the yellow, and a new watermark or header isn't drawn over the old one. The
|
||||
/// page's content and its other kinds of mark stay.
|
||||
#[test]
|
||||
fn update_previews_the_page_without_the_marks_it_replaces() {
|
||||
let uploads = Uploads::default();
|
||||
let pdf = content_fixture();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).renderer(uploads.clone()).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("doc.pdf", None, pdf.clone()).unwrap();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
let yellow = egui::Color32::from_rgb(255, 255, 153);
|
||||
let red = |c: egui::Color32| c.r() > 200 && c.g() < 80 && c.b() < 80;
|
||||
let blue = |c: egui::Color32| c.b() > 150 && c.r() < 100 && c.g() < 100;
|
||||
let dark = |c: egui::Color32| c.r() < 60 && c.g() < 60 && c.b() < 60;
|
||||
let apply = |h: &mut Harness<'static, PdfCraftApp>, command: &str, draft: &dyn Fn(&mut pdfcraft_ui_egui::marks::MarksDraft)| {
|
||||
assert!(h.state_mut().execute(command));
|
||||
h.run_steps(2);
|
||||
draft(&mut h.state_mut().marks_draft);
|
||||
h.run_steps(2);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
};
|
||||
// A pale yellow background, a red watermark and a blue header.
|
||||
apply(&mut h, "edit.background", &|d| d.bg.color = [1.0, 1.0, 0.6]);
|
||||
apply(&mut h, "edit.watermark", &|d| {
|
||||
d.wm.text = "OLD".into();
|
||||
d.wm.color = [1.0, 0.0, 0.0];
|
||||
d.wm.opacity = 1.0;
|
||||
d.wm.rotation = 0.0;
|
||||
});
|
||||
apply(&mut h, "edit.header_footer", &|d| {
|
||||
d.hf.text[1] = "OLD".into();
|
||||
d.hf.color = [0.0, 0.0, 1.0];
|
||||
d.hf.font_size = 40.0;
|
||||
});
|
||||
// Add keeps the marks there, and previews on the thumbnail, which has all three.
|
||||
assert!(h.state_mut().execute("edit.watermark"));
|
||||
h.run_steps(2);
|
||||
let all = |i: &egui::ColorImage| at(i, 0.5, 0.75) == yellow && count(i, red) > 200 && count(i, blue) > 40;
|
||||
wait_for_preview(&mut h, &uploads, "the page with its marks", all);
|
||||
h.get_by_label("Cancel").click();
|
||||
h.run_steps(2);
|
||||
|
||||
assert!(h.state_mut().execute("edit.background.update"));
|
||||
h.run_steps(2);
|
||||
h.state_mut().marks_draft.bg.color = [0.6, 0.8, 1.0];
|
||||
let (tint, page) = wait_for_preview(&mut h, &uploads, "a page", |_| true);
|
||||
assert_eq!(tint, egui::Color32::from_rgb(153, 204, 255), "the new colour tints the page");
|
||||
assert_eq!(at(&page, 0.5, 0.75), egui::Color32::WHITE, "the page is previewed without the yellow background");
|
||||
assert!(count(&page, red) > 200 && count(&page, blue) > 40 && count(&page, dark) > 1000, "the watermark, header and page content stay");
|
||||
h.get_by_label("Cancel").click();
|
||||
h.run_steps(2);
|
||||
|
||||
assert!(h.state_mut().execute("edit.watermark.update"));
|
||||
h.run_steps(2);
|
||||
h.state_mut().marks_draft.wm.text = "NEW".into();
|
||||
let (_, page) = wait_for_preview(&mut h, &uploads, "a page", |_| true);
|
||||
assert_eq!(count(&page, red), 0, "the new watermark isn't previewed over the old one");
|
||||
assert!(at(&page, 0.5, 0.75) == yellow && count(&page, blue) > 40, "the background and header stay");
|
||||
h.get_by_label("Cancel").click();
|
||||
h.run_steps(2);
|
||||
|
||||
assert!(h.state_mut().execute("edit.header_footer.update"));
|
||||
h.run_steps(2);
|
||||
let (_, page) = wait_for_preview(&mut h, &uploads, "a page", |_| true);
|
||||
assert_eq!(count(&page, blue), 0, "the new header isn't previewed over the old one");
|
||||
assert!(at(&page, 0.5, 0.75) == yellow && count(&page, red) > 200, "the background and watermark stay");
|
||||
}
|
||||
|
||||
/// Pages from 1 to 1 stays one page: the dialog used to turn "to" back into the last page on
|
||||
/// every frame while both were 1, so only page 1 could not be chosen alone. The field starts on
|
||||
/// the last page.
|
||||
|
||||
@@ -2423,8 +2423,8 @@ milestone = "M7"
|
||||
status = "shipped"
|
||||
commands = ["edit.header_footer"]
|
||||
tools = ["doc_header_footer"]
|
||||
evidence = ["crates/ui-egui/tests/it/marks.rs::header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset", "crates/edit/src/tests.rs::header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"]
|
||||
notes = "Standard faces only: Helvetica, Times or Courier (#3754; no bold, italic or embedded fonts yet); drawn in display space on rotated pages; artifact-tagged; the dialog previews the text over the page's own thumbnail (the current page, or the first one the range selects), in the interface font and with tokens unexpanded"
|
||||
evidence = ["crates/ui-egui/tests/it/marks.rs::header_and_footer_dialog_inserts_tokens_and_applies_to_a_subset", "crates/edit/src/tests.rs::header_and_footer_are_drawn_in_display_space_and_wrap_the_original_content", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"]
|
||||
notes = "Standard faces only: Helvetica, Times or Courier (#3754; no bold, italic or embedded fonts yet); drawn in display space on rotated pages; artifact-tagged; the dialog previews the text over the page's own thumbnail (the current page, or the first one the range selects), in the interface font and with tokens unexpanded; Update previews it on the page without the headers and footers it replaces"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.header-footer-update-remove"
|
||||
@@ -2483,8 +2483,8 @@ milestone = "M7"
|
||||
status = "shipped"
|
||||
commands = ["edit.watermark"]
|
||||
tools = ["doc_watermark"]
|
||||
evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/it/automation.rs::headers_watermarks_and_backgrounds_through_tools", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"]
|
||||
notes = "Helvetica; fit-to-page or a size; the dialog previews it over the page's own thumbnail, always on top (a watermark behind the page is not drawn under its content there)"
|
||||
evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/it/automation.rs::headers_watermarks_and_backgrounds_through_tools", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"]
|
||||
notes = "Helvetica; fit-to-page or a size; the dialog previews it over the page's own thumbnail, always on top (a watermark behind the page is not drawn under its content there); Update previews on the page without the watermarks it replaces"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.watermark-add-file"
|
||||
@@ -2531,8 +2531,8 @@ milestone = "M7"
|
||||
status = "shipped"
|
||||
commands = ["edit.background"]
|
||||
tools = ["doc_background"]
|
||||
evidence = ["crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/engine/src/tests.rs::backgrounds_and_watermarks_from_files", "crates/ui-egui/tests/it/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"]
|
||||
notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page; the dialog previews a colour as a tint of the page's own thumbnail (opaque pictures on the page are tinted there too, not in the result), a file as its frame and name"
|
||||
evidence = ["crates/ui-egui/tests/it/marks.rs::watermark_and_background_dialogs", "crates/engine/src/tests.rs::headers_footers_watermarks_and_backgrounds_show_update_and_remove", "crates/engine/src/tests.rs::backgrounds_and_watermarks_from_files", "crates/ui-egui/tests/it/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright", "crates/ui-egui/tests/it/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks", "crates/ui-egui/tests/it/marks.rs::update_previews_the_page_without_the_marks_it_replaces", "crates/engine/src/tests.rs::update_previews_on_the_page_without_the_marks_it_replaces"]
|
||||
notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page; the dialog previews a colour as a tint of the page's own thumbnail (opaque pictures on the page are tinted there too, not in the result), a file as its frame and name; Update previews on the page without the backgrounds it replaces, not tinting the old one"
|
||||
|
||||
[[feature]]
|
||||
id = "edit.background-update-remove"
|
||||
|
||||
Reference in New Issue
Block a user