From 27a2ac47cffcb1fdb9d43b5a30657250b6164f9e Mon Sep 17 00:00:00 2001 From: Guillem Hermida Date: Fri, 16 Oct 2026 16:04:41 +0200 Subject: [PATCH 1/4] fix(ui): the header, footer and watermark previews show the page under the marks (#2837) The Header and Footer, Watermark and Background dialogs drew their preview on a blank sheet, so you could not see where the marks would land on the page's text and pictures. The preview now paints the page's own thumbnail, rendered off the UI thread like the pages panel's, with a blank sheet until it arrives. It shows the current page when the page range selects it, otherwise the first page the range selects, at its displayed size (after /Rotate), which is the frame the engine lays the marks out in. A background colour tints the page picture instead of covering it, as a background behind the content looks. Fixes #2837. --- crates/ui-egui/src/marks_ui.rs | 85 +++++++++++++--- crates/ui-egui/tests/ui_group/marks.rs | 130 ++++++++++++++++++++++++- parity/acrobat-features.toml | 14 +-- 3 files changed, 207 insertions(+), 22 deletions(-) diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index 4a60c40a6..e994c7911 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -1,6 +1,7 @@ //! Add Header and Footer, Add Watermark and Add Background dialogs (Acrobat: Edit a PDF ▸ …; -//! dialog snapshots d13, d14, d12d). Each has the shared Page Range Options and a schematic -//! preview of the current page. "Update" opens the same dialog in replace mode. +//! dialog snapshots d13, d14, d12d). Each has the shared Page Range Options and a preview of a +//! page the range selects, with the marks laid out on it. "Update" opens the same dialog in +//! replace mode. use egui::{Align, Color32, CornerRadius, Layout, Rect, Stroke, pos2, vec2}; use pdfcraft_engine::{Background, Edit, HeaderFooter, MarkKind, Watermark}; @@ -123,14 +124,30 @@ fn color_button(ui: &mut egui::Ui, c: &mut [f64; 3]) { } } -/// A schematic of the current page with the marks laid out (no rendering needed). -fn preview(ui: &mut egui::Ui, t: &Tokens, size: (f64, f64), draw: impl FnOnce(&egui::Painter, Rect, f32)) { +/// The page (displayed size `size`) with the marks laid out on it. `picture` is its thumbnail, +/// rendered off the UI thread like the pages panel's; until it arrives the page is a blank sheet. +/// `tint` multiplies the page, which is how a background behind its content looks. +fn preview( + ui: &mut egui::Ui, + t: &Tokens, + size: (f64, f64), + picture: Option, + tint: Color32, + 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 (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); - painter.rect_filled(page, CornerRadius::ZERO, Color32::WHITE); + match picture { + Some(tex) => { + painter.image(tex, page, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), tint); + } + None => { + painter.rect_filled(page, CornerRadius::ZERO, tint); + } + } painter.rect_stroke(page, CornerRadius::ZERO, Stroke::new(1.0, t.border), egui::StrokeKind::Outside); draw(&painter, page, s); } @@ -140,13 +157,26 @@ fn rgb32(c: [f64; 3]) -> Color32 { Color32::from_rgb(b(c[0]), b(c[1]), b(c[2])) } +/// The tint of a page over a background of `color` at `opacity`: the colour shows where the page +/// is white and the content stays on top. A non-finite opacity is opaque, as the engine has it. +fn behind(color: [f64; 3], opacity: f64) -> Color32 { + let a = if opacity.is_finite() { opacity.clamp(0.0, 1.0) } else { 1.0 }; + rgb32(color.map(|c| 1.0 - a * (1.0 - c))) +} + +/// 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 { + let pages = range.pages(count); + if pages.contains(¤t) { current } else { pages.first().copied().unwrap_or(current) } +} + /// The dialog for `kind`; returns (apply, cancel). pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: MarkKind) -> (bool, bool) { let Some((i, id)) = app.active_ids() else { return (false, true) }; let current = app.views[i].current; let Some(doc) = app.session.get(id) else { return (false, true) }; let count = doc.info.pages.len(); - let page = doc.info.pages.get(current).map(|p| (p.width as f64, p.height as f64)).unwrap_or((612.0, 792.0)); let units = app.units; let d = &mut app.marks_draft; let mut browse = false; @@ -305,12 +335,21 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ui.label(egui::RichText::new(tl!("Page Range Options")).font(theme::semibold(12.5))); d.range.ui(ui, count); ui.add_space(8.0); - // Preview of the current page. + // The preview: the current page, 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. + let target = preview_page(&d.range, 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 tint = if kind == MarkKind::Background && !d.use_file { behind(d.bg.color, d.bg.opacity) } else { Color32::WHITE }; let hf = d.hf.clone(); - let (wm, bg, fit) = (d.wm.clone(), d.bg.clone(), d.fit); + let (wm, fit) = (d.wm.clone(), d.fit); let file_name = d.use_file.then(|| d.file.as_ref().map(|f| f.0.clone())).flatten(); ui.horizontal(|ui| { - preview(ui, t, page, |p, r, s| match kind { + preview(ui, t, page, picture, tint, |p, r, s| match kind { MarkKind::HeaderFooter => { for (k, text) in hf.text.iter().enumerate() { if text.trim().is_empty() { @@ -362,15 +401,14 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M Color32::GRAY, ); } - MarkKind::Background => { - p.rect_filled(r, CornerRadius::ZERO, rgb32(bg.color).gamma_multiply(bg.opacity as f32)); - } + // A colour is the page's tint. + MarkKind::Background => {} }); ui.vertical(|ui| { ui.label( egui::RichText::new(crate::i18n::fmt( tl!("Preview: page {p} of {n}"), - &[("p", &(current + 1).to_string()), ("n", &count.to_string())], + &[("p", &(target + 1).to_string()), ("n", &count.to_string())], )) .small() .color(t.text_faint), @@ -521,6 +559,27 @@ mod tests { assert_eq!(r.pages(5), [0, 2, 4]); } + #[test] + fn the_preview_shows_a_page_the_range_selects() { + let all = PageRange::default(); + assert_eq!(preview_page(&all, 2, 5), 2, "the current page when the range selects it"); + let even = PageRange { all: true, subset: Subset::Even, ..PageRange::default() }; + assert_eq!(preview_page(&even, 0, 5), 1, "otherwise the first page the range selects"); + let later = PageRange { all: false, from: 3, to: 4, subset: Subset::All }; + assert_eq!(preview_page(&later, 0, 5), 2); + let none = PageRange { all: false, from: 4, to: 2, subset: Subset::All }; + assert_eq!(preview_page(&none, 1, 5), 1, "the current page when the range selects none"); + assert_eq!(preview_page(&all, 0, 0), 0, "no pages at all"); + } + + #[test] + fn a_background_tints_the_page_it_is_behind() { + assert_eq!(behind([1.0, 0.0, 0.0], 1.0), Color32::from_rgb(255, 0, 0)); + assert_eq!(behind([1.0, 0.0, 0.0], 0.0), Color32::WHITE); + assert_eq!(behind([0.0, 0.0, 0.0], 0.5), Color32::from_rgb(128, 128, 128)); + assert_eq!(behind([0.2, 0.4, 0.6], f64::NAN), behind([0.2, 0.4, 0.6], 1.0), "opaque, as the engine has it"); + } + #[test] fn the_watermark_edit_carries_the_offset() { let mut d = MarksDraft::default(); diff --git a/crates/ui-egui/tests/ui_group/marks.rs b/crates/ui-egui/tests/ui_group/marks.rs index 0a0e45b15..fa5d7f884 100644 --- a/crates/ui-egui/tests/ui_group/marks.rs +++ b/crates/ui-egui/tests/ui_group/marks.rs @@ -15,10 +15,14 @@ trailer << /Root 1 0 R >> %%EOF"; fn harness() -> Harness<'static, PdfCraftApp> { - let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + harness_with(FIXTURE.to_vec()) +} + +fn harness_with(pdf: Vec) -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); app.set_option("language", "en").unwrap(); - app.open_bytes("doc.pdf", None, FIXTURE.to_vec()).unwrap(); + app.open_bytes("doc.pdf", None, pdf.clone()).unwrap(); app }); h.run_steps(4); @@ -96,3 +100,125 @@ fn backgrounds_and_watermarks_from_a_file() { let s = h.state(); assert_eq!(s.session.get(s.views[0].id).unwrap().can_undo(), Some("Add background")); } + +/// Two 300 × 400 pages with content (a dark bar), the second turned a quarter (/Rotate 90), so it +/// is shown landscape, 400 × 300. +fn content_fixture() -> Vec { + let bar = "0 0 0 rg 40 300 220 60 re f"; + format!( + "%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 300 400] >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /Contents 5 0 R >> endobj +4 0 obj << /Type /Page /Parent 2 0 R /Rotate 90 /Contents 5 0 R >> endobj +5 0 obj << /Length {} >> +stream +{bar} +endstream +endobj +trailer << /Root 1 0 R >> +%%EOF", + bar.len() + ) + .into_bytes() +} + +/// The pictures of page `page` (its thumbnail) painted last frame: paint order, bounds, tint. +fn page_pictures(h: &Harness<'static, PdfCraftApp>, page: usize) -> Vec<(usize, egui::Rect, egui::Color32)> { + let Some(tex) = h.state().views[0].thumb(page).map(|t| t.id()) else { return Vec::new() }; + h.output() + .shapes + .iter() + .enumerate() + .filter_map(|(k, s)| match &s.shape { + egui::Shape::Mesh(m) if m.texture_id == tex => Some((k, m.calc_bounds(), m.vertices.first()?.color)), + _ => None, + }) + .collect() +} + +/// The preview's picture of `page`, once its thumbnail has arrived (it renders off the UI thread). +fn wait_for_picture(h: &mut Harness<'static, PdfCraftApp>, page: usize) -> (usize, egui::Rect, egui::Color32) { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); + loop { + h.step(); + if let [one] = page_pictures(h, page)[..] { + return one; + } + assert!(std::time::Instant::now() < deadline, "the preview never showed page {}'s picture", page + 1); + std::thread::sleep(std::time::Duration::from_millis(5)); + } +} + +/// A text painted on the page last frame (the dialog's text boxes show it too): its paint order +/// and its laid-out rectangle. +fn painted(h: &Harness<'static, PdfCraftApp>, text: &str, page: egui::Rect) -> (usize, egui::Rect) { + h.output() + .shapes + .iter() + .enumerate() + .find_map(|(k, s)| match &s.shape { + egui::Shape::Text(t) if t.galley.text() == text => { + Some((k, t.galley.rect.translate(t.pos.to_vec2()))).filter(|(_, r)| page.contains(r.center())) + } + _ => None, + }) + .unwrap_or_else(|| panic!("{text:?} is painted on the page")) +} + +fn near(a: f32, b: f32, what: &str) { + assert!((a - b).abs() < 1.0, "{what}: {a} against {b}"); +} + +#[test] +fn the_preview_shows_the_page_the_range_selects_under_the_marks() { + use pdfcraft_ui_egui::marks::{PageRange, Subset}; + let mut h = harness_with(content_fixture()); + assert!(h.state_mut().execute("edit.header_footer")); + h.run_steps(2); + { + let d = &mut h.state_mut().marks_draft; + d.hf.text[1] = "TOP".into(); + d.hf.text[5] = "END".into(); + // Page 2 alone, not the current page: the preview follows the range (#2837). + d.range = PageRange { all: false, from: 2, to: 2, subset: Subset::All }; + } + let (at, page, tint) = wait_for_picture(&mut h, 1); + h.get_by_label("Preview: page 2 of 2"); + assert_eq!(tint, egui::Color32::WHITE); + assert!((page.width() / page.height() - 4.0 / 3.0).abs() < 0.01, "page 2 is shown turned, as displayed: {page:?}"); + // The marks are drawn over the page, where the engine puts them on its displayed box + // (default margins: 36 pt top and bottom, 72 pt left and right). + let s = page.width() / 400.0; + let (top_at, top) = painted(&h, "TOP", page); + let (end_at, end) = painted(&h, "END", page); + assert!(at < top_at && at < end_at, "the page is under the marks"); + near(top.center().x, page.center().x, "centre header, across"); + near(top.center().y, page.top() + 36.0 * s, "centre header, down"); + near(end.right(), page.right() - 72.0 * s, "right footer, across"); + near(end.center().y, page.bottom() - 36.0 * s, "right footer, down"); + h.get_by_label("Cancel").click(); + h.run_steps(2); + + // The watermark sits on the page's centre. + assert!(h.state_mut().execute("edit.watermark")); + h.run_steps(2); + { + let d = &mut h.state_mut().marks_draft; + d.wm.text = "DRAFT".into(); + d.wm.rotation = 0.0; + } + let (at, page, _) = wait_for_picture(&mut h, 1); + let (mark_at, mark) = painted(&h, "DRAFT", page); + assert!(at < mark_at, "the page is under the watermark"); + near(mark.center().x, page.center().x, "watermark, across"); + near(mark.center().y, page.center().y, "watermark, down"); + h.get_by_label("Cancel").click(); + h.run_steps(2); + + // A background colour shows where the page is white: it tints the page's picture. + assert!(h.state_mut().execute("edit.background")); + h.run_steps(2); + let (_, _, tint) = wait_for_picture(&mut h, 1); + assert_eq!(tint, egui::Color32::from_rgb(255, 255, 217), "the default pale yellow, opaque"); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index c3d319f94..c5bb50420 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -2388,8 +2388,8 @@ milestone = "M7" status = "shipped" commands = ["edit.header_footer"] tools = ["doc_header_footer"] -evidence = ["crates/ui-egui/tests/ui_group/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"] -notes = "Helvetica only (no font choice yet); drawn in display space on rotated pages; artifact-tagged" +evidence = ["crates/ui-egui/tests/ui_group/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/ui_group/marks.rs::the_preview_shows_the_page_the_range_selects_under_the_marks"] +notes = "Helvetica only (no font choice 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" [[feature]] id = "edit.header-footer-update-remove" @@ -2448,8 +2448,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/ui_group/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/automation.rs::headers_watermarks_and_backgrounds_through_tools"] -notes = "Helvetica; fit-to-page or a size" +evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/ui-egui/tests/ui_group/marks.rs::watermark_and_background_dialogs", "crates/automation/tests/automation.rs::headers_watermarks_and_backgrounds_through_tools", "crates/ui-egui/tests/ui_group/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)" [[feature]] id = "edit.watermark-add-file" @@ -2461,7 +2461,7 @@ status = "shipped" commands = ["edit.watermark"] tools = ["doc_watermark"] evidence = ["crates/engine/src/tests.rs::backgrounds_and_watermarks_from_files"] -notes = "an image or a page of a PDF, scaled relative to the page, rotated, with opacity, behind or on top" +notes = "an image or a page of a PDF, scaled relative to the page, rotated, with opacity, behind or on top; the dialog's preview shows its frame and file name over the page, not the picture itself" [[feature]] id = "edit.watermark-update-remove" @@ -2496,8 +2496,8 @@ milestone = "M7" status = "shipped" commands = ["edit.background"] tools = ["doc_background"] -evidence = ["crates/ui-egui/tests/ui_group/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/ui_group/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright"] -notes = "colour, or a picture from a file (an image or a page of a PDF) scaled relative to the page" +evidence = ["crates/ui-egui/tests/ui_group/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/ui_group/marks.rs::backgrounds_and_watermarks_from_a_file", "crates/organize/src/tests.rs::a_page_becomes_a_form_xobject_upright", "crates/ui-egui/tests/ui_group/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" [[feature]] id = "edit.background-update-remove" From bde5c37058869a6389d3b4dc5cf3d78a8aa55552 Mon Sep 17 00:00:00 2001 From: Guillem Hermida Date: Fri, 23 Oct 2026 13:11:16 +0200 Subject: [PATCH 2/4] deps: ctutils 0.4.5 for RUSTSEC-2026-0385 --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47d73be44..9129e85d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1291,9 +1291,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", "subtle", From bf81191122fdb3d072879707e90de215b4ca7846 Mon Sep 17 00:00:00 2001 From: Guillem Hermida Date: Fri, 23 Oct 2026 12:49:33 +0200 Subject: [PATCH 3/4] tests: the pending-open control test waits for the worker by time, not by 30 frames --- crates/ui-egui/tests/it/control.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/tests/it/control.rs b/crates/ui-egui/tests/it/control.rs index 54bfe2fa3..aa71d411b 100644 --- a/crates/ui-egui/tests/it/control.rs +++ b/crates/ui-egui/tests/it/control.rs @@ -1672,15 +1672,19 @@ fn control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has() assert_eq!(first["pending"], true, "{first}"); // The worker completes the open on later frames; the tab shows, and only then does a - // repeated open focus it instead of reporting another pending open. + // repeated open focus it instead of reporting another pending open. The worker is a thread + // of its own, so the wait is bounded by time: a busy runner can draw 30 frames before it has + // read the file. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30); let mut documents = 0; - for _ in 0..30 { + while std::time::Instant::now() < deadline { h.run_steps(1); let state = ok(&mut h, &c, "ui.state", json!({})); documents = state["documents"].as_array().map(|d| d.len()).unwrap_or_default(); if documents == 1 { break; } + std::thread::sleep(std::time::Duration::from_millis(5)); } assert_eq!(documents, 1, "the pending open completed into a tab"); From 7668a2c378cb2a65abfa49c13fe839a3504e2a95 Mon Sep 17 00:00:00 2001 From: Guillem Hermida Date: Fri, 23 Oct 2026 19:18:36 +0200 Subject: [PATCH 4/4] tests: each forms test saves into a folder of its own tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value (its same-frame case) and typing_and_saving_in_the_same_frame_saves_the_last_keystrokes both asked scratch() for "same-frame". The tests of the it binary share one process id and run in parallel, so they got the same folder, and one could clear it while the other was saving into it: the save then failed to read back with NotFound. scratch() now adds a number taken per call. --- crates/ui-egui/tests/it/forms.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/tests/it/forms.rs b/crates/ui-egui/tests/it/forms.rs index 230addd65..8e1a3b760 100644 --- a/crates/ui-egui/tests/it/forms.rs +++ b/crates/ui-egui/tests/it/forms.rs @@ -500,8 +500,13 @@ fn wait_for_file(h: &mut Harness<'static, PdfCraftApp>, path: &std::path::Path) h.run_steps(1); } +/// A fresh folder of its own for one test. The tests of this binary share one process id and run +/// in parallel, and two of them name their folder "same-frame": a number taken per call keeps one +/// test from clearing the folder another is saving into. fn scratch(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("pdfcraft-forms-{name}-{}", std::process::id())); + static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!("pdfcraft-forms-{name}-{}-{n}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); dir