mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 22:19:10 -06:00
fix(ui): header, footer and watermark previews step through the pages (#4448)
* fix(ui): header, footer and watermark previews step through the pages The marks dialogs preview the current page, or the first page the range selects when it leaves the current one out, with no way to look at another page. Arrows beside the page label step the preview to the previous or next page the range selects. The page stepped to resets each time the dialog opens. * fix(ui): the marks preview forgets a stepped-to page the range leaves out
This commit is contained in:
@@ -390,6 +390,7 @@ impl PdfCraftApp {
|
||||
K::Background
|
||||
};
|
||||
self.marks_draft.replace = id.ends_with(".update");
|
||||
self.marks_draft.preview = None;
|
||||
self.dialog = Some(Dialog::Marks(kind));
|
||||
}
|
||||
"edit.bates" => {
|
||||
@@ -400,6 +401,7 @@ impl PdfCraftApp {
|
||||
self.marks_draft.hf.text[5] = "<<Bates Number#6#1##>>".into();
|
||||
}
|
||||
self.marks_draft.focused_box = 5;
|
||||
self.marks_draft.preview = None;
|
||||
self.dialog = Some(Dialog::Marks(pdfcraft_engine::MarkKind::HeaderFooter));
|
||||
}
|
||||
"edit.header_footer.remove" | "edit.watermark.remove" | "edit.background.remove" => {
|
||||
|
||||
@@ -7,7 +7,7 @@ use egui::{Align, Color32, CornerRadius, Layout, Rect, Stroke, pos2, vec2};
|
||||
use pdfcraft_engine::{Background, Edit, FontFamily, HeaderFooter, MarkKind, Watermark};
|
||||
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{PdfCraftApp, widgets};
|
||||
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;
|
||||
@@ -90,6 +90,9 @@ pub struct MarksDraft {
|
||||
pub use_file: bool,
|
||||
pub file: Option<(String, std::sync::Arc<Vec<u8>>)>,
|
||||
pub file_page: usize,
|
||||
/// The page (0-based) the preview was stepped to; `None` previews the current page. Reset
|
||||
/// each time the dialog opens.
|
||||
pub preview: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for MarksDraft {
|
||||
@@ -107,6 +110,7 @@ impl Default for MarksDraft {
|
||||
use_file: false,
|
||||
file: None,
|
||||
file_page: 1,
|
||||
preview: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +176,24 @@ fn preview_page(range: &PageRange, current: usize, count: usize) -> usize {
|
||||
if pages.contains(¤t) { current } else { pages.first().copied().unwrap_or(current) }
|
||||
}
|
||||
|
||||
/// [`preview_page`] for the page stepped to (`stepped`), or the current page if none. A page
|
||||
/// stepped to that the range leaves out is replaced by the page shown, so widening the range again
|
||||
/// keeps that page rather than jumping back.
|
||||
fn shown_preview(range: &PageRange, stepped: &mut Option<usize>, current: usize, count: usize) -> usize {
|
||||
let shown = preview_page(range, stepped.unwrap_or(current), count);
|
||||
if stepped.is_some() {
|
||||
*stepped = Some(shown);
|
||||
}
|
||||
shown
|
||||
}
|
||||
|
||||
/// The page the preview steps to from `shown`: the range's next page after it (`forward`) or the
|
||||
/// last one before it, or `None` at either end.
|
||||
fn step_preview(range: &PageRange, shown: usize, count: usize, forward: bool) -> Option<usize> {
|
||||
let pages = range.pages(count);
|
||||
if forward { pages.into_iter().find(|&p| p > shown) } else { pages.into_iter().rev().find(|&p| p < shown) }
|
||||
}
|
||||
|
||||
/// 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) };
|
||||
@@ -343,10 +365,10 @@ 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);
|
||||
// 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);
|
||||
// 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.
|
||||
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);
|
||||
@@ -425,14 +447,24 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
||||
MarkKind::Background => {}
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("Preview: page {p} of {n}"),
|
||||
&[("p", &(target + 1).to_string()), ("n", &count.to_string())],
|
||||
))
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
ui.horizontal(|ui| {
|
||||
// Steps through the pages the range selects.
|
||||
let (back, next) = (step_preview(&d.range, target, count, false), step_preview(&d.range, target, count, true));
|
||||
if ui.add_enabled_ui(back.is_some(), |ui| icons::button(ui, "chevron-left", 22.0, false, tl!("Previous page"))).inner.clicked() {
|
||||
d.preview = back.or(d.preview);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(
|
||||
tl!("Preview: page {p} of {n}"),
|
||||
&[("p", &(target + 1).to_string()), ("n", &count.to_string())],
|
||||
))
|
||||
.small()
|
||||
.color(t.text_faint),
|
||||
);
|
||||
if ui.add_enabled_ui(next.is_some(), |ui| icons::button(ui, "chevron-right", 22.0, false, tl!("Next page"))).inner.clicked() {
|
||||
d.preview = next.or(d.preview);
|
||||
}
|
||||
});
|
||||
let n = d.range.pages(count).len();
|
||||
let applies =
|
||||
if n == 1 { tl!("Applies to 1 page").to_string() } else { crate::i18n::fmt(tl!("Applies to {n} pages"), &[("n", &n.to_string())]) };
|
||||
@@ -597,6 +629,40 @@ mod tests {
|
||||
assert_eq!(preview_page(&all, 0, 0), 0, "no pages at all");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preview_steps_through_the_pages_the_range_selects() {
|
||||
let odd = PageRange { all: true, subset: Subset::Odd, ..PageRange::default() };
|
||||
// Pages 1, 3 and 5 (0-based 0, 2, 4) of 5.
|
||||
assert_eq!(step_preview(&odd, 0, 5, true), Some(2));
|
||||
assert_eq!(step_preview(&odd, 2, 5, true), Some(4));
|
||||
assert_eq!(step_preview(&odd, 4, 5, true), None, "the last page has no next");
|
||||
assert_eq!(step_preview(&odd, 4, 5, false), Some(2));
|
||||
assert_eq!(step_preview(&odd, 0, 5, false), None, "the first page has no previous");
|
||||
// From a page the range leaves out, to the nearest one it selects on that side.
|
||||
assert_eq!(step_preview(&odd, 1, 5, true), Some(2));
|
||||
assert_eq!(step_preview(&odd, 1, 5, false), Some(0));
|
||||
assert_eq!(step_preview(&odd, 0, 0, true), None, "no pages at all");
|
||||
// A page stepped to is previewed while the range selects it, and the range's first page
|
||||
// once a new range leaves it out.
|
||||
assert_eq!(preview_page(&odd, 2, 5), 2);
|
||||
let later = PageRange { all: false, from: 4, to: 5, subset: Subset::All };
|
||||
assert_eq!(preview_page(&later, 2, 5), 3);
|
||||
// Narrowing the range past the page stepped to keeps the page shown instead, also once
|
||||
// the range is widened again.
|
||||
let mut stepped = Some(4);
|
||||
let first_two = PageRange { all: false, from: 1, to: 2, subset: Subset::All };
|
||||
assert_eq!(shown_preview(&first_two, &mut stepped, 1, 5), 0);
|
||||
assert_eq!(stepped, Some(0));
|
||||
assert_eq!(shown_preview(&PageRange::default(), &mut stepped, 1, 5), 0);
|
||||
// A page stepped to inside the range stays; with none stepped to, the current page shows.
|
||||
let mut stepped = Some(2);
|
||||
assert_eq!(shown_preview(&odd, &mut stepped, 0, 5), 2);
|
||||
assert_eq!(stepped, Some(2));
|
||||
let mut stepped = None;
|
||||
assert_eq!(shown_preview(&odd, &mut stepped, 4, 5), 4);
|
||||
assert_eq!(stepped, None);
|
||||
}
|
||||
|
||||
#[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));
|
||||
|
||||
@@ -265,6 +265,35 @@ fn the_preview_shows_the_page_the_range_selects_under_the_marks() {
|
||||
assert_eq!(tint, egui::Color32::from_rgb(255, 255, 217), "the default pale yellow, opaque");
|
||||
}
|
||||
|
||||
/// The preview's page stepper, beside its "page N of M" label: the dialog's own button, not the
|
||||
/// page navigation's of the same name.
|
||||
fn preview_step(h: &mut Harness<'static, PdfCraftApp>, label: &str, now: &str) {
|
||||
let row = h.get_by_label(now).rect().center().y;
|
||||
h.query_all_by_label(label).find(|n| (n.rect().center().y - row).abs() < 15.0).unwrap_or_else(|| panic!("{label} beside {now:?}")).click();
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preview_steps_through_the_pages_and_starts_at_the_current_one() {
|
||||
let mut h = harness();
|
||||
h.state_mut().views[0].go_to_page(1);
|
||||
h.run_steps(2);
|
||||
assert!(h.state_mut().execute("edit.watermark"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Preview: page 2 of 3");
|
||||
preview_step(&mut h, "Next page", "Preview: page 2 of 3");
|
||||
h.get_by_label("Preview: page 3 of 3");
|
||||
preview_step(&mut h, "Previous page", "Preview: page 3 of 3");
|
||||
preview_step(&mut h, "Previous page", "Preview: page 2 of 3");
|
||||
h.get_by_label("Preview: page 1 of 3");
|
||||
// Opened again, the preview is back at the current page.
|
||||
h.get_by_label("Cancel").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state_mut().execute("edit.watermark"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Preview: page 2 of 3");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user