mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:35:21 +00:00
M3: full screen clicks on fields and notes, Shift+Ctrl first and last page, no redraw while a page renders
- A click on a link, form field or comment turns no page in full screen: the pointer stays active to click links and open notes (Adobe Acrobat Help, 2019, p. 65). - Shift+Ctrl+Page Up / Up Arrow go to the first page and Shift+Ctrl+Page Down / Down Arrow to the last, as Home and End (p. 83). - A transition waiting for its incoming page looks again every 50 ms rather than asking for every frame; the render cache wakes the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
689b5790c4
commit
94d3e628b0
@@ -911,6 +911,9 @@ struct PageOut {
|
||||
open_signature: bool,
|
||||
open_table: Option<(usize, [f64; 4])>,
|
||||
primary_down: bool,
|
||||
/// The pointer is over a link, comment, field or what is placed on the page: a click there is
|
||||
/// for it, and turns no page in full screen.
|
||||
on_object: bool,
|
||||
refused: Option<String>,
|
||||
stamp_placed: bool,
|
||||
}
|
||||
@@ -2082,6 +2085,7 @@ fn draw_pages(
|
||||
open_signature,
|
||||
open_table,
|
||||
primary_down: _,
|
||||
on_object: _,
|
||||
refused,
|
||||
stamp_placed,
|
||||
} = page_out;
|
||||
@@ -2097,6 +2101,8 @@ fn draw_pages(
|
||||
let now = ui.input(|i| i.time);
|
||||
hide_pointer = presenter::frame(view, info, painter, ui.ctx(), now, animates, pointer);
|
||||
}
|
||||
// Read before the claims are spent below, and before the pointer hides, which claims the top layer.
|
||||
let on_object = view.cursor.over_object(tool);
|
||||
// A still pointer hides over a link too, and its tip with it: the top layer, which every tool reaches.
|
||||
if hide_pointer {
|
||||
view.cursor.claim(Layer::Drag, egui::CursorIcon::None);
|
||||
@@ -2269,6 +2275,7 @@ fn draw_pages(
|
||||
open_signature,
|
||||
open_table,
|
||||
primary_down: ptr.primary_down,
|
||||
on_object,
|
||||
refused,
|
||||
stamp_placed,
|
||||
};
|
||||
@@ -2770,12 +2777,14 @@ fn frame_overlays(ui: &mut egui::Ui, view: &mut DocView, env: &PageEnv<'_>, prim
|
||||
/// Full screen: the click turns the page unless it did something else first. It waits out the
|
||||
/// double-click time: turning on the first click's release sent a double-click's second click,
|
||||
/// meant to select a word, to the next page (review on #2070). A selection cancels it, and so do a
|
||||
/// link followed meanwhile and leaving full screen; a double-click elsewhere still turns once.
|
||||
/// click on a link, field or comment (`clicked_object`) and leaving full screen: the pointer stays
|
||||
/// active "so that you can click links and open notes" (*Adobe Acrobat Help*, 2019, p. 65), and
|
||||
/// fields to fill. A double-click elsewhere still turns once.
|
||||
/// `click` is `Some(back)` on the frame of a click, `back` when it was a shift-click.
|
||||
fn schedule_presentation_turn(ui: &egui::Ui, view: &mut DocView, click: Option<bool>, link_followed: bool) {
|
||||
fn schedule_presentation_turn(ui: &egui::Ui, view: &mut DocView, click: Option<bool>, clicked_object: bool) {
|
||||
let now = ui.input(|i| i.time);
|
||||
if let Some(back) = click
|
||||
&& !link_followed
|
||||
&& !clicked_object
|
||||
&& view.selection.is_none()
|
||||
&& view.clicks < 2
|
||||
{
|
||||
@@ -2783,7 +2792,7 @@ fn schedule_presentation_turn(ui: &egui::Ui, view: &mut DocView, click: Option<b
|
||||
view.present_turn = Some((now + delay, !back));
|
||||
}
|
||||
if let Some((due, forward)) = view.present_turn {
|
||||
if link_followed || view.selection.is_some() || view.presentation.is_none() {
|
||||
if clicked_object || view.selection.is_some() || view.presentation.is_none() {
|
||||
view.present_turn = None;
|
||||
} else if now >= due {
|
||||
view.present_turn = None;
|
||||
@@ -3281,6 +3290,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
open_signature,
|
||||
open_table,
|
||||
primary_down,
|
||||
on_object,
|
||||
mut refused,
|
||||
stamp_placed,
|
||||
} = page_out;
|
||||
@@ -3354,7 +3364,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
Some(comments::CanvasAction::CopyImageFile(p, i)) => image_action = Some(crate::edit_text_ui::ImageAction::CopyFile(p, i)),
|
||||
None => {}
|
||||
}
|
||||
schedule_presentation_turn(ui, view, present, clicked_link.is_some());
|
||||
schedule_presentation_turn(ui, view, present, clicked_link.is_some() || on_object && present.is_some());
|
||||
let remote_open = follow_link(app, index, clicked_link);
|
||||
if tool == QuickTool::Crop && cropped {
|
||||
tool = QuickTool::Select;
|
||||
|
||||
@@ -391,18 +391,26 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s
|
||||
view.find = None;
|
||||
}
|
||||
}
|
||||
if key(Key::Home) {
|
||||
// First and last page: Home and End, or Shift+Ctrl+Page Up / Up Arrow and Shift+Ctrl+Page Down /
|
||||
// Down Arrow (*Adobe Acrobat Help*, 2019, "Keys for navigating a PDF", p. 83). These take the
|
||||
// key from the page turning and scrolling below.
|
||||
let (command, shift) = ctx.input(|i| (i.modifiers.command, i.modifiers.shift));
|
||||
let to_end = command && shift && (key(Key::PageDown) || key(Key::ArrowDown));
|
||||
let to_start = command && shift && (key(Key::PageUp) || key(Key::ArrowUp));
|
||||
if key(Key::Home) || to_start {
|
||||
view.go_to_page(0);
|
||||
}
|
||||
if key(Key::End) {
|
||||
if key(Key::End) || to_end {
|
||||
view.go_to_page(usize::MAX);
|
||||
}
|
||||
if to_end || to_start {
|
||||
return;
|
||||
}
|
||||
// Full screen: Space and Enter go forward a page, and Shift+Enter and Backspace back
|
||||
// (*Adobe Acrobat Help*, 2019, "Read a document in Full Screen mode", p. 66: "To go to the
|
||||
// previous page, press Shift+Enter"). The arrows and Page Up / Page Down already turn pages
|
||||
// where nothing scrolls, below.
|
||||
if view.presentation.is_some() {
|
||||
let shift = ctx.input(|i| i.modifiers.shift);
|
||||
if key(Key::Space) || (key(Key::Enter) && !shift) {
|
||||
view.step_page(true);
|
||||
}
|
||||
@@ -412,7 +420,6 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s
|
||||
}
|
||||
// As in Acrobat: → / ← go to the next / previous page in every layout (#185), and so do
|
||||
// ⌘Page Down / ⌘Page Up, or plain Page Down / Page Up in single-page view.
|
||||
let command = ctx.input(|i| i.modifiers.command);
|
||||
let scrolls = view.continuous;
|
||||
if key(Key::ArrowRight) || (key(Key::PageDown) && (command || !scrolls)) {
|
||||
view.step_page(true);
|
||||
|
||||
@@ -109,6 +109,16 @@ impl PageCursor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the pointer is over something a press with `tool` acts on in its own right: a
|
||||
/// link, a comment, a field, or what is placed on the page and its grips and editors. Not the
|
||||
/// page and its text, nor a drag, whose layer also hides the pointer in full screen.
|
||||
pub(crate) fn over_object(&self, tool: QuickTool) -> bool {
|
||||
LAYERS
|
||||
.into_iter()
|
||||
.zip(&self.claims)
|
||||
.any(|(layer, claim)| claim.is_some() && (Layer::Link..Layer::Drag).contains(&layer) && layer.reached_by(tool))
|
||||
}
|
||||
|
||||
/// The claim on the highest layer `tool` reaches.
|
||||
fn winner(claims: [Option<Claim>; LAYERS.len()], tool: QuickTool) -> Option<Claim> {
|
||||
LAYERS.into_iter().zip(claims).rev().find_map(|(layer, claim)| claim.filter(|_| layer.reached_by(tool)))
|
||||
|
||||
@@ -158,8 +158,12 @@ pub(super) fn frame(
|
||||
paint(painter, &turn.from, from_rect, &f.outgoing);
|
||||
if let Some(tex) = &incoming {
|
||||
paint(painter, tex, to_rect, &f.incoming);
|
||||
ctx.request_repaint();
|
||||
} else {
|
||||
// Nothing moves until the page arrives, and the render cache wakes the window
|
||||
// while pages are queued: look again now and then rather than every frame.
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(50));
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2046,6 +2046,41 @@ fn a_transition_into_a_page_that_failed_to_render_ends_at_once() {
|
||||
assert!(!v.presenting_a_transition(), "no wait for a page that won't render");
|
||||
}
|
||||
|
||||
/// While the incoming page is still rendering, the presenter holds the page before without asking
|
||||
/// for a frame after frame: the render cache wakes the window while pages are queued.
|
||||
#[test]
|
||||
fn a_transition_waiting_for_its_page_does_not_redraw_every_frame() {
|
||||
use pdfcraft_render::{PageTransition, TransitionStyle};
|
||||
let page = pdfcraft_render::PageInfo { width: 300.0, height: 400.0, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation: 0 };
|
||||
let wipe = PageTransition {
|
||||
page: 1,
|
||||
style: Some(TransitionStyle::Wipe),
|
||||
duration: 1.0,
|
||||
horizontal: false,
|
||||
inward: false,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display: None,
|
||||
};
|
||||
let info = DocInfo { pages: vec![page.clone(), page], transitions: vec![wipe], ..Default::default() };
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { layout: PageLayout::Single, continuous: false, ..Default::default() });
|
||||
v.enter_presentation();
|
||||
let ctx = egui::Context::default();
|
||||
let tex = ctx.load_texture("page", egui::ColorImage::new([4, 4], vec![Color32::WHITE; 16]), Default::default());
|
||||
v.pages.insert(0, super::cache::PageTex { tag: 0, tex });
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
v.current = 1;
|
||||
v.screen_rects = vec![(1, Rect::from_min_size(Pos2::ZERO, egui::vec2(300.0, 400.0)))];
|
||||
// egui asks for its first frames itself: past them, only the presenter asks.
|
||||
for now in [0.1, 0.2, 0.3] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
let again = presenter_frame(&ctx, &mut v, &info, 0.4);
|
||||
assert!(v.presenting_a_transition(), "still waiting for page 2");
|
||||
assert!(!again, "without asking for a frame after frame");
|
||||
}
|
||||
|
||||
/// A display time on the first page of the last two-page spread can't advance (there is no spread
|
||||
/// after it): the presenter then stays on it without asking for a frame after frame.
|
||||
#[test]
|
||||
|
||||
@@ -2514,3 +2514,90 @@ fn full_screen_hides_a_still_pointer_over_a_link() {
|
||||
h.run_steps(16);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "still for over three seconds");
|
||||
}
|
||||
|
||||
/// A text field on page 1 of three.
|
||||
const FIELD_PAGES: &[u8] = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] >> >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R 4 0 R 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /Annots [6 0 R] >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
5 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
6 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [50 100 250 140] /P 3 0 R /F 4 >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
/// Full screen keeps the pointer active "so that you can click links and open notes" (*Adobe
|
||||
/// Acrobat Help*, 2019, p. 65): a click on a form field opens it for typing rather than turning
|
||||
/// the page, and a click beside it still turns.
|
||||
#[test]
|
||||
fn full_screen_click_on_a_field_fills_it_and_turns_no_page() {
|
||||
let mut h = harness(&[]);
|
||||
h.state_mut().open_bytes("field.pdf", None, FIELD_PAGES.to_vec()).expect("opens");
|
||||
h.run_steps(8);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
let n = h.state().views.len() - 1;
|
||||
let r = h.state().views[n].page_screen_rect(0).expect("page 1 on screen");
|
||||
let on_field = egui::pos2(r.left() + r.width() * 0.5, r.top() + r.height() * (400.0 - 120.0) / 400.0);
|
||||
click_with(&mut h, on_field, Modifiers::NONE);
|
||||
h.run_steps(8);
|
||||
assert_eq!(h.state().views[n].current, 0, "no page turned");
|
||||
assert!(h.ctx.egui_wants_keyboard_input(), "the field is open for typing");
|
||||
click_with(&mut h, egui::pos2(r.center().x, r.top() + 20.0), Modifiers::NONE);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[n].current, 1, "a click beside the field turns the page");
|
||||
}
|
||||
|
||||
/// Shift+Ctrl+Page Down or Down Arrow goes to the last page, and Shift+Ctrl+Page Up or Up Arrow
|
||||
/// to the first, as Home and End do (*Adobe Acrobat Help*, 2019, "Keys for navigating a PDF",
|
||||
/// p. 83), in full screen and out of it.
|
||||
#[test]
|
||||
fn shift_ctrl_page_keys_and_arrows_go_to_the_first_and_last_page() {
|
||||
for full_screen in [false, true] {
|
||||
let mut h = harness(&[("layout", "single")]);
|
||||
if full_screen {
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
}
|
||||
let both = Modifiers::COMMAND | Modifiers::SHIFT;
|
||||
for (k, want) in [(Key::PageDown, 2), (Key::PageUp, 0), (Key::ArrowDown, 2), (Key::ArrowUp, 0)] {
|
||||
h.key_press_modifiers(both, k);
|
||||
h.run_steps(6);
|
||||
assert_eq!(h.state().views[0].current, want, "{k:?}, full screen {full_screen}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In full screen a double-click on a note opens it to edit, and turns no page: the pointer stays
|
||||
/// active "so that you can click links and open notes" (*Adobe Acrobat Help*, 2019, p. 65).
|
||||
#[test]
|
||||
fn full_screen_double_click_on_a_note_opens_it_and_turns_no_page() {
|
||||
const NOTE: &[u8] = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 /MediaBox [0 0 300 400] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /Annots [4 0 R] >> endobj
|
||||
4 0 obj << /Type /Annot /Subtype /Text /Rect [100 100 124 124] /Contents (Hello) /T (A) /F 4 >> endobj
|
||||
5 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
let mut h = harness_stepping(1.0 / 60.0, &[]);
|
||||
h.state_mut().open_bytes("note.pdf", None, NOTE.to_vec()).expect("opens");
|
||||
h.run_steps(8);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
let n = h.state().views.len() - 1;
|
||||
let r = h.state().views[n].page_screen_rect(0).expect("page 1 on screen");
|
||||
let p = egui::pos2(r.left() + r.width() * 112.0 / 300.0, r.top() + r.height() * (400.0 - 112.0) / 400.0);
|
||||
h.hover_at(p);
|
||||
h.run_steps(2);
|
||||
for _ in 0..2 {
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
}
|
||||
h.step();
|
||||
}
|
||||
// Past the double-click time, when a turn would have come.
|
||||
h.run_steps(30);
|
||||
assert_eq!(h.state().views[n].current, 0, "no page turned");
|
||||
assert!(h.ctx.egui_wants_keyboard_input(), "the note is open to edit");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user