mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 13:01:29 +00:00
Merge #5316: M3: full screen plays page transitions and display times, and its keys and clicks go the right way (#379, #5315)
# Conflicts: # crates/ui-egui/src/canvas/tests.rs
This commit is contained in:
commit
d40c626b6e
11 files changed
+1301
-28
No files matched your search
@@ -32,6 +32,7 @@ pub(super) mod page_cursor;
|
||||
mod page_input;
|
||||
pub(super) mod page_pointer;
|
||||
pub(super) mod page_router;
|
||||
mod presenter;
|
||||
pub mod scroll_speed;
|
||||
mod selection;
|
||||
pub(super) mod signature_drag;
|
||||
@@ -567,6 +568,8 @@ pub struct DocView {
|
||||
/// Full screen (Acrobat's Full Screen Mode): the display saved on entering it, while the view
|
||||
/// shows one page or spread at a time, fitted to the screen. `None` outside full screen.
|
||||
presentation: Option<SavedDisplay>,
|
||||
/// Full screen's transitions, display times and pointer (see [`presenter`]).
|
||||
pub(crate) presenter: presenter::Presenter,
|
||||
pages: HashMap<usize, PageTex>,
|
||||
/// Pages the renderer could not draw, with the reason (never re-requested).
|
||||
errors: HashMap<usize, String>,
|
||||
@@ -1044,6 +1047,7 @@ impl DocView {
|
||||
compare_marks: Vec::new(),
|
||||
interface: Default::default(),
|
||||
presentation: None,
|
||||
presenter: Default::default(),
|
||||
pages: HashMap::new(),
|
||||
errors: HashMap::new(),
|
||||
cut_pages: Default::default(),
|
||||
@@ -1222,6 +1226,8 @@ impl DocView {
|
||||
fresh.interface = self.interface;
|
||||
// Still presenting, with the display to go back to on leaving.
|
||||
fresh.presentation = self.presentation.take();
|
||||
// A new document: none of the old one's transition, page or timer carries over.
|
||||
fresh.presenter = Default::default();
|
||||
fresh.back = std::mem::take(&mut self.back);
|
||||
fresh.forward = std::mem::take(&mut self.forward);
|
||||
fresh.find = self.find.take();
|
||||
@@ -1328,6 +1334,8 @@ struct PageEnv<'a> {
|
||||
glide: Vec2,
|
||||
hand: bool,
|
||||
info: &'a DocInfo,
|
||||
/// The Animation effects preference as it applies now (full screen's transitions follow it).
|
||||
animates: bool,
|
||||
initials: &'a Option<crate::fill_sign::SavedSig>,
|
||||
key_scroll: f32,
|
||||
key_side: f32,
|
||||
@@ -1386,6 +1394,9 @@ struct PageOut {
|
||||
page_action: Option<PageMenuAction>,
|
||||
paste_comments_at: Option<(usize, [f64; 2])>,
|
||||
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>,
|
||||
/// The current page or spread where it is drawn, every page of it: one off screen (the second
|
||||
/// page of a zoomed spread) has no screen transform, but still decides where the row ends.
|
||||
@@ -1723,9 +1734,12 @@ fn draw_page(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_>, i
|
||||
}
|
||||
let pcx = comments::PageCx { page: i, xf: &xf, ptr, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark };
|
||||
// A cancelled press takes nothing until the button is up, though the page's menu still opens
|
||||
// where a right-click is.
|
||||
let input = if view.press_cancelled {
|
||||
if let Some(p) = frame.right_click_on(|p| xf.rect.contains(p)) {
|
||||
// where a right-click is. A page full screen doesn't show yet takes nothing at all. (A press
|
||||
// there gives nothing on the page an owner, so its release, once the page is in, takes nothing
|
||||
// either.)
|
||||
let veiled = page_input::veiled(view);
|
||||
let input = if view.press_cancelled || veiled {
|
||||
if !veiled && let Some(p) = frame.right_click_on(|p| xf.rect.contains(p)) {
|
||||
comments::anchor(&pcx, p, view);
|
||||
}
|
||||
PageInput {
|
||||
@@ -1802,6 +1816,7 @@ fn draw_pages(
|
||||
y_shift,
|
||||
..
|
||||
} = *env;
|
||||
let animates = env.animates;
|
||||
// egui's drag responses accept every pointer button. Keep a wheel gesture from also
|
||||
// selecting text, drawing a mark, or panning with the Hand tool.
|
||||
if middle_gesture {
|
||||
@@ -1927,6 +1942,7 @@ fn draw_pages(
|
||||
open_table,
|
||||
page_action: _,
|
||||
primary_down: _,
|
||||
on_object: _,
|
||||
refused,
|
||||
row_on_screen: _,
|
||||
stamp_placed,
|
||||
@@ -1939,8 +1955,20 @@ fn draw_pages(
|
||||
let selected = view.prepare.selected.as_ref().map(|(name, _)| name.as_str());
|
||||
crate::calc_order_ui::paint_trace(ui, painter, visible, &view.calc_order, generation, selected);
|
||||
}
|
||||
// Full screen plays transitions over the page just drawn, and keeps time (#379).
|
||||
let mut hide_pointer = false;
|
||||
if view.presentation.is_some() {
|
||||
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);
|
||||
}
|
||||
// The pages' one cursor, from the claim on the highest layer the tool reaches, and the tip.
|
||||
let hover_text = pointer.filter(|_| unobstructed).zip(view.cursor.resolve(ui.ctx(), tool));
|
||||
let hover_text = pointer.filter(|_| unobstructed && !hide_pointer).zip(view.cursor.resolve(ui.ctx(), tool));
|
||||
// ScrollArea ignores the wheel while any widget is dragged, so a text-selection drag
|
||||
// scrolls the page itself (#721). After the page loop, so it keeps scrolling once the
|
||||
// pointer leaves the selection's page for the gap or the next page; over the whole
|
||||
@@ -1997,7 +2025,8 @@ fn draw_pages(
|
||||
// The row as it is now: in continuous view the current page may have changed above.
|
||||
let row_on_screen =
|
||||
shown_row(view, env.rects).iter().filter_map(|&i| env.rects.get(i)).map(|r| r.translate(origin.to_vec2())).reduce(Rect::union);
|
||||
resp.context_menu(|ui| {
|
||||
let presenting = view.presentation.is_some();
|
||||
page_context_menu(&resp, presenting, |ui| {
|
||||
// Crop mode has no menu: a right-click on the picture being cropped, its box or its grips is
|
||||
// the crop's (#5835).
|
||||
if matches!(menu_for, Some(Owner::Picture { part: PicturePart::Crop(_), .. })) {
|
||||
@@ -2166,6 +2195,7 @@ fn draw_pages(
|
||||
page_action,
|
||||
paste_comments_at,
|
||||
primary_down: ptr.primary_down,
|
||||
on_object,
|
||||
refused,
|
||||
row_on_screen,
|
||||
stamp_placed,
|
||||
@@ -2790,12 +2820,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
|
||||
{
|
||||
@@ -2803,7 +2835,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;
|
||||
@@ -2884,11 +2916,25 @@ fn shown_row(view: &DocView, rects: &[Rect]) -> Vec<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pages' context menu, except in full screen, where a right-click turns back a page instead
|
||||
/// ([`presentation_click_state`]).
|
||||
fn page_context_menu(resp: &egui::Response, presenting: bool, add: impl FnOnce(&mut egui::Ui)) {
|
||||
if !presenting {
|
||||
resp.context_menu(add);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full screen: a click on the document goes forward a page and a shift-click back, as in
|
||||
/// Acrobat, unless the click did something else (a link, a word selected). Decided once the pages
|
||||
/// have had the click. `Some(back)` on the frame of a click, `back` when it was a shift-click.
|
||||
fn presentation_click_state(ui: &egui::Ui, view: &DocView, navigation_rect: Rect, enabled: bool) -> Option<bool> {
|
||||
let clicked = view.presentation.is_some() && enabled && presentation_click(ui, navigation_rect);
|
||||
let presenting = view.presentation.is_some() && enabled;
|
||||
// A right-click goes back a page, as a left click goes forward (*Adobe Acrobat Help*, 2019,
|
||||
// "Full Screen preferences", p. 69).
|
||||
if presenting && ui.rect_contains_pointer(navigation_rect) && ui.input(|i| i.pointer.secondary_clicked()) {
|
||||
return Some(true);
|
||||
}
|
||||
let clicked = presenting && presentation_click(ui, navigation_rect);
|
||||
// Shift as held now, or as the click's own event carried it (a synthetic click).
|
||||
clicked.then(|| {
|
||||
ui.input(|i| {
|
||||
@@ -3264,6 +3310,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
let env = PageEnv {
|
||||
added: &added,
|
||||
allowed,
|
||||
animates,
|
||||
author: &author,
|
||||
auto_delta,
|
||||
avail,
|
||||
@@ -3354,6 +3401,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
page_action,
|
||||
paste_comments_at,
|
||||
primary_down,
|
||||
on_object,
|
||||
mut refused,
|
||||
row_on_screen,
|
||||
stamp_placed,
|
||||
@@ -3442,7 +3490,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;
|
||||
|
||||
@@ -423,21 +423,31 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s
|
||||
}
|
||||
}
|
||||
// Home and End, and the keys listed with them for Go to first / last page: Shift+Ctrl
|
||||
// (Shift+⌘ on macOS) with ↑ / ↓ or Page Up / Page Down (#6434).
|
||||
// (Shift+⌘ on macOS) with ↑ / ↓ or Page Up / Page Down (#6434; *Adobe Acrobat Help*, 2019,
|
||||
// "Keys for navigating a PDF", p. 83). These take the key from the page turning and
|
||||
// scrolling below.
|
||||
let shift_cmd = |k| KeyboardShortcut::new(Modifiers::COMMAND | Modifiers::SHIFT, k);
|
||||
if key(Key::Home) || pressed(shift_cmd(Key::ArrowUp)) || pressed(shift_cmd(Key::PageUp)) {
|
||||
let to_start = pressed(shift_cmd(Key::ArrowUp)) || pressed(shift_cmd(Key::PageUp));
|
||||
let to_end = pressed(shift_cmd(Key::ArrowDown)) || pressed(shift_cmd(Key::PageDown));
|
||||
if key(Key::Home) || to_start {
|
||||
view.go_to_page(0);
|
||||
}
|
||||
if key(Key::End) || pressed(shift_cmd(Key::ArrowDown)) || pressed(shift_cmd(Key::PageDown)) {
|
||||
if key(Key::End) || to_end {
|
||||
view.go_to_end();
|
||||
}
|
||||
// Full screen: Space and Enter go forward a page and Backspace back, as in Acrobat (the
|
||||
// arrows and Page Up / Page Down already turn pages where nothing scrolls, below).
|
||||
if to_end || to_start {
|
||||
return;
|
||||
}
|
||||
let (command, shift) = ctx.input(|i| (i.modifiers.command, i.modifiers.shift));
|
||||
// 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() {
|
||||
if key(Key::Space) || key(Key::Enter) {
|
||||
if key(Key::Space) || (key(Key::Enter) && !shift) {
|
||||
view.step_page(true);
|
||||
}
|
||||
if key(Key::Backspace) {
|
||||
if key(Key::Backspace) || (key(Key::Enter) && shift) {
|
||||
view.step_page(false);
|
||||
}
|
||||
}
|
||||
@@ -446,7 +456,6 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s
|
||||
// than the window (a plan zoomed in) → / ← scroll sideways a line first, and turn the page
|
||||
// once there is nothing left to scroll that way, as ↓ / ↑ do in single-page view; ⌘→ / ⌘←
|
||||
// turn it at once.
|
||||
let command = ctx.input(|i| i.modifiers.command);
|
||||
let scrolls = view.continuous;
|
||||
let line = KEY_SCROLL_LINE * speed.factor();
|
||||
// Only an up-to-date view counts: no jump pending.
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -825,6 +825,13 @@ fn finish_text_pointer_gesture(ui: &egui::Ui, view: &mut DocView, text: &PageTex
|
||||
}
|
||||
}
|
||||
|
||||
/// Full screen is turning to the current page, or waiting for it to render: the page under the
|
||||
/// pointer isn't the one on screen, so its links, fields, text and comments take no input.
|
||||
/// Turning the page (a click, a key) is handled apart and still works.
|
||||
pub(super) fn veiled(view: &DocView) -> bool {
|
||||
view.presentation.is_some() && !view.presenter.settled_on(view.current)
|
||||
}
|
||||
|
||||
/// Everything drawn over the page after its text: pictures, comments, content, links, fields, marks
|
||||
/// and badges.
|
||||
pub(super) fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_>, slot: &PageSlot, input: &PageInput) {
|
||||
@@ -869,8 +876,9 @@ pub(super) fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut P
|
||||
}
|
||||
crate::image_pick::drop_stale(doc, view);
|
||||
crate::image_pick::paint(painter, &xf, i, info, view);
|
||||
// A cancelled press ends no selection: no markup or redaction is made from it.
|
||||
if !view.press_cancelled {
|
||||
// A cancelled press ends no selection: no markup or redaction is made from it, nor from a
|
||||
// page full screen doesn't show yet.
|
||||
if !view.press_cancelled && !veiled(view) {
|
||||
comments::page_after_text(resp, &pcx, view);
|
||||
if tool == QuickTool::Redact && can_modify {
|
||||
crate::redact_ui::after_text(resp, i, info, view);
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
//! Full screen as a presentation (ISO 32000-2 §12.4.4): a page's transition (`/Trans`, Table 164)
|
||||
//! plays when the page comes into view, a page with a display time (`/Dur`) advances by itself,
|
||||
//! and the pointer hides while it stays still.
|
||||
//!
|
||||
//! Transitions play in single-page view without view rotation, from the outgoing page's raster
|
||||
//! to the incoming one's once it has rendered; spreads, a rotated view and animations turned off
|
||||
//! (the Animation effects preference, which by default follows the system's request for no
|
||||
//! animations) change pages at once. A style the spec doesn't name is its default, Replace, a
|
||||
//! cut. Display times count from the end of the transition and stop at the last page; the keys
|
||||
//! and clicks still turn pages meanwhile (§12.4.4.1, NOTE 1), and leaving full screen stops them.
|
||||
//! The file's numbers are bounded: a transition plays for at most `MAX_TRANSITION` seconds, and a
|
||||
//! display time waits at most `MAX_DISPLAY`.
|
||||
|
||||
use egui::{Color32, CornerRadius, Painter, Pos2, Rect, TextureHandle, pos2};
|
||||
use pdfcraft_render::transition::{self, Effect, Piece};
|
||||
use pdfcraft_render::{DocInfo, PageTransition};
|
||||
|
||||
use super::{DocView, PageLayout};
|
||||
|
||||
/// Seconds the pointer stays still before full screen hides it.
|
||||
pub(crate) const HIDE_POINTER_AFTER: f64 = 3.0;
|
||||
/// Bounds on a document's numbers, which are untrusted: the longest transition played and the
|
||||
/// longest display time waited for (a day: a timer can't hold the largest number a file can
|
||||
/// write).
|
||||
const MAX_TRANSITION: f64 = 10.0;
|
||||
const MAX_DISPLAY: f64 = 24.0 * 3600.0;
|
||||
|
||||
/// What full screen remembers between frames.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Presenter {
|
||||
/// The page wholly in view, and since when (its transition over).
|
||||
shown: Option<(usize, f64)>,
|
||||
turn: Option<Turn>,
|
||||
/// Where the pointer last moved to, and when.
|
||||
pointer: Option<(Pos2, f64)>,
|
||||
}
|
||||
|
||||
/// A transition under way, from one page to the next.
|
||||
struct Turn {
|
||||
from_page: usize,
|
||||
/// The outgoing page's raster, kept while the transition plays.
|
||||
from: TextureHandle,
|
||||
to_page: usize,
|
||||
effect: Effect,
|
||||
seconds: f64,
|
||||
/// When the incoming page's raster arrived and the transition began.
|
||||
started: Option<f64>,
|
||||
}
|
||||
|
||||
impl Presenter {
|
||||
/// Whether a page transition is playing or waiting for its incoming page.
|
||||
pub(crate) fn turning(&self) -> bool {
|
||||
self.turn.is_some()
|
||||
}
|
||||
|
||||
/// Whether `page` is the page wholly in view: not while it comes in or waits for its raster,
|
||||
/// nor in the frame that turns to it (this frame's [`frame`] hasn't caught up yet). While a
|
||||
/// page turns, `shown` is `None`, so a transition under way needs no check of its own.
|
||||
pub(crate) fn settled_on(&self, page: usize) -> bool {
|
||||
self.shown.is_some_and(|(p, _)| p == page)
|
||||
}
|
||||
}
|
||||
|
||||
/// The transition and display time of page `page` (0-based), if it has either.
|
||||
fn entry(info: &DocInfo, page: usize) -> Option<&PageTransition> {
|
||||
info.transitions.iter().find(|t| t.page == page)
|
||||
}
|
||||
|
||||
/// The effect a page's transition plays and its length in seconds; `None` for no visible effect.
|
||||
fn effect_of(t: &PageTransition) -> Option<(Effect, f64)> {
|
||||
let style = t.style?;
|
||||
let effect = Effect::from_parts(style.name(), t.horizontal, t.inward, t.direction, t.scale);
|
||||
let seconds = f64::from(t.duration);
|
||||
(effect != Effect::Replace && seconds.is_finite() && seconds > 0.0).then(|| (effect, seconds.min(MAX_TRANSITION)))
|
||||
}
|
||||
|
||||
/// `rect` with the aspect ratio of a `w` by `h` page, as large as fits in `within` and centred
|
||||
/// there.
|
||||
fn fitted(w: f32, h: f32, within: Rect) -> Rect {
|
||||
let (w, h) = (w.max(1.0), h.max(1.0));
|
||||
let k = (within.width() / w).min(within.height() / h);
|
||||
if !k.is_finite() || k <= 0.0 {
|
||||
return within;
|
||||
}
|
||||
Rect::from_center_size(within.center(), egui::vec2(w * k, h * k))
|
||||
}
|
||||
|
||||
/// Where a piece of a page goes on screen and which part of its raster it shows: `dst` in `page`
|
||||
/// (screen), `src` as texture coordinates, and its tint. `None` for a piece with nothing to show.
|
||||
pub(crate) fn piece_on_screen(page: Rect, p: &Piece) -> Option<(Rect, Rect, Color32)> {
|
||||
let alpha = if p.alpha.is_finite() { p.alpha.clamp(0.0, 1.0) } else { 0.0 };
|
||||
let at = |f: [f32; 4]| {
|
||||
let f = f.map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 });
|
||||
(pos2(f[0], f[1]), pos2(f[2], f[3]))
|
||||
};
|
||||
let (d0, d1) = at(p.dst);
|
||||
let (s0, s1) = at(p.src);
|
||||
let dst = Rect::from_min_max(page.lerp_inside(d0.to_vec2()), page.lerp_inside(d1.to_vec2()));
|
||||
let uv = Rect::from_min_max(s0, s1);
|
||||
(alpha > 0.0 && dst.width() > 0.0 && dst.height() > 0.0).then(|| (dst, uv, Color32::WHITE.gamma_multiply(alpha)))
|
||||
}
|
||||
|
||||
fn paint(painter: &Painter, tex: &TextureHandle, page: Rect, pieces: &[Piece]) {
|
||||
for p in pieces {
|
||||
if let Some((dst, uv, tint)) = piece_on_screen(page, p) {
|
||||
painter.image(tex.id(), dst, uv, tint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One frame of full screen for `view`, after its pages were drawn: start, paint or finish the
|
||||
/// current page's transition, and advance a page whose display time is up. Returns whether the
|
||||
/// pointer has been still long enough to hide: the caller hides it once the page has chosen its
|
||||
/// cursor, which would otherwise show it again over a link. `animates` is the Animation effects
|
||||
/// preference as it applies now.
|
||||
pub(super) fn frame(
|
||||
view: &mut DocView,
|
||||
info: &DocInfo,
|
||||
painter: &Painter,
|
||||
ctx: &egui::Context,
|
||||
now: f64,
|
||||
animates: bool,
|
||||
pointer: Option<Pos2>,
|
||||
) -> bool {
|
||||
let page = view.current;
|
||||
let showing = view.presenter.shown.map(|(p, _)| p);
|
||||
let arriving = view.presenter.turn.as_ref().map(|t| t.to_page);
|
||||
if showing != Some(page) && arriving != Some(page) {
|
||||
// A new page: play its transition from the page before, when there is one to play.
|
||||
let plays = animates && view.layout == PageLayout::Single && view.rotation == 0;
|
||||
let turn = showing.or(arriving).filter(|_| plays).and_then(|from_page| {
|
||||
let (effect, seconds) = entry(info, page).and_then(effect_of)?;
|
||||
let from = view.pages.get(&from_page)?.tex.clone();
|
||||
Some(Turn { from_page, from, to_page: page, effect, seconds, started: None })
|
||||
});
|
||||
// Its display time starts once the page is on screen, not while it still says Rendering…
|
||||
// (or at once for a page the renderer can't draw, which never will be). In two-page view
|
||||
// that is every page of the spread, so the spread isn't turned before both were shown.
|
||||
let drawn = |p: &usize| view.pages.contains_key(p) || view.errors.contains_key(p);
|
||||
let ready = drawn(&page) && view.screen_rects.iter().all(|(p, _)| drawn(p));
|
||||
view.presenter.shown = if turn.is_some() || !ready { None } else { Some((page, now)) };
|
||||
view.presenter.turn = turn;
|
||||
}
|
||||
|
||||
if let Some(turn) = view.presenter.turn.as_mut() {
|
||||
// A page the renderer can't draw ends the turn: waiting for it would hold the page before
|
||||
// forever, and its display time would never start.
|
||||
let failed = view.errors.contains_key(&turn.to_page);
|
||||
let to_rect = view.screen_rects.iter().find(|(i, _)| *i == turn.to_page).map(|&(_, r)| r).filter(|_| !failed);
|
||||
let incoming = view.pages.get(&turn.to_page).map(|p| p.tex.clone());
|
||||
match to_rect {
|
||||
None => {
|
||||
view.presenter.turn = None;
|
||||
view.presenter.shown = Some((page, now));
|
||||
}
|
||||
Some(to_rect) => {
|
||||
// The outgoing page keeps its own shape, centred where the incoming one is.
|
||||
let from_rect = info.pages.get(turn.from_page).map_or(to_rect, |p| fitted(p.width, p.height, to_rect.union(view.viewport_screen)));
|
||||
let from_rect = if (from_rect.aspect_ratio() - to_rect.aspect_ratio()).abs() < 0.01 { to_rect } else { from_rect };
|
||||
let t = match (&incoming, turn.started) {
|
||||
// Hold the outgoing page until the incoming one has rendered.
|
||||
(None, _) => 0.0,
|
||||
(Some(_), Some(start)) => ((now - start) / turn.seconds) as f32,
|
||||
(Some(_), None) => {
|
||||
turn.started = Some(now);
|
||||
0.0
|
||||
}
|
||||
};
|
||||
if t >= 1.0 {
|
||||
view.presenter.turn = None;
|
||||
view.presenter.shown = Some((page, now));
|
||||
} else {
|
||||
let f = transition::frame(&turn.effect, t);
|
||||
painter.rect_filled(from_rect.union(to_rect), CornerRadius::ZERO, Color32::BLACK);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A page with a display time advances once it is up (not past the last page).
|
||||
if let Some((shown, since)) = view.presenter.shown
|
||||
&& shown == page
|
||||
&& page + 1 < info.pages.len()
|
||||
&& let Some(seconds) = entry(info, page).and_then(|t| t.display).map(f64::from).filter(|d| d.is_finite() && *d >= 0.0)
|
||||
{
|
||||
let left = since + seconds.min(MAX_DISPLAY) - now;
|
||||
if left <= 0.0 {
|
||||
// In two-page view the last spread can't advance: then there is nothing to redraw.
|
||||
let before = view.current;
|
||||
view.step_page(true);
|
||||
if view.current != before {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
} else {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(left));
|
||||
}
|
||||
}
|
||||
|
||||
// The pointer hides once it has been still for a while, and comes back when it moves.
|
||||
match pointer {
|
||||
None => view.presenter.pointer = None,
|
||||
Some(p) if view.presenter.pointer.map(|(q, _)| q) != Some(p) => view.presenter.pointer = Some((p, now)),
|
||||
Some(_) => {}
|
||||
}
|
||||
let Some((_, since)) = view.presenter.pointer else { return false };
|
||||
let left = since + HIDE_POINTER_AFTER - now;
|
||||
if left > 0.0 {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(left));
|
||||
}
|
||||
left <= 0.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn page() -> Rect {
|
||||
Rect::from_min_size(pos2(100.0, 50.0), egui::vec2(200.0, 400.0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_piece_maps_page_fractions_to_the_screen_and_its_raster() {
|
||||
let p = Piece { src: [0.0, 0.0, 0.5, 1.0], dst: [0.5, 0.0, 1.0, 1.0], alpha: 1.0 };
|
||||
let (dst, uv, tint) = piece_on_screen(page(), &p).unwrap();
|
||||
assert_eq!(dst, Rect::from_min_max(pos2(200.0, 50.0), pos2(300.0, 450.0)));
|
||||
assert_eq!(uv, Rect::from_min_max(pos2(0.0, 0.0), pos2(0.5, 1.0)));
|
||||
assert_eq!(tint, Color32::WHITE);
|
||||
let faded = piece_on_screen(page(), &Piece { alpha: 0.5, ..p }).unwrap().2;
|
||||
assert!(faded.a() > 0 && faded.a() < 255, "half transparent: {faded:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_pieces_paint_nothing_or_stay_on_the_page() {
|
||||
for p in [
|
||||
Piece { src: [0.0; 4], dst: [0.2, 0.2, 0.2, 0.9], alpha: 1.0 },
|
||||
Piece { src: [0.0, 0.0, 1.0, 1.0], dst: [0.0, 0.0, 1.0, 1.0], alpha: 0.0 },
|
||||
Piece { src: [0.0, 0.0, 1.0, 1.0], dst: [0.0, 0.0, 1.0, 1.0], alpha: f32::NAN },
|
||||
] {
|
||||
assert!(piece_on_screen(page(), &p).is_none(), "{p:?}");
|
||||
}
|
||||
let (dst, uv, _) =
|
||||
piece_on_screen(page(), &Piece { src: [f32::NAN, -3.0, 9.0, f32::INFINITY], dst: [-1.0, f32::NAN, 2.0, 1.5], alpha: 1.0 }).unwrap();
|
||||
assert!(page().contains_rect(dst), "{dst:?}");
|
||||
assert!(Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)).contains_rect(uv), "{uv:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_visible_effect_with_a_length_plays() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
let t = |style, duration| PageTransition {
|
||||
page: 1,
|
||||
style,
|
||||
duration,
|
||||
horizontal: true,
|
||||
inward: true,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display: None,
|
||||
};
|
||||
assert_eq!(effect_of(&t(Some(TransitionStyle::Wipe), 1.5)), Some((Effect::Wipe { direction: 0 }, 1.5)));
|
||||
assert_eq!(effect_of(&t(Some(TransitionStyle::Fade), 600.0)).map(|e| e.1), Some(MAX_TRANSITION), "capped");
|
||||
for (style, duration) in
|
||||
[(None, 1.0), (Some(TransitionStyle::Replace), 1.0), (Some(TransitionStyle::Fade), 0.0), (Some(TransitionStyle::Fade), f32::NAN)]
|
||||
{
|
||||
assert_eq!(effect_of(&t(style, duration)), None, "{style:?} {duration}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_outgoing_page_keeps_its_shape() {
|
||||
let r = fitted(600.0, 300.0, Rect::from_min_size(pos2(0.0, 0.0), egui::vec2(400.0, 400.0)));
|
||||
assert_eq!(r, Rect::from_min_max(pos2(0.0, 100.0), pos2(400.0, 300.0)));
|
||||
assert_eq!(fitted(0.0, f32::NAN, Rect::ZERO), Rect::ZERO);
|
||||
}
|
||||
}
|
||||
@@ -2724,6 +2724,363 @@ fn first_page_now_measures_the_pages_in_the_spread() {
|
||||
assert!(v.first_page_now(&info).is_none());
|
||||
}
|
||||
|
||||
/// Full screen's presenter for one frame of `ctx`, at time `now`: whether the frame asked to be
|
||||
/// drawn again at once.
|
||||
fn presenter_frame(ctx: &egui::Context, v: &mut DocView, info: &DocInfo, now: f64) -> bool {
|
||||
presenter_frame_animating(ctx, v, info, now, true)
|
||||
}
|
||||
|
||||
/// [`presenter_frame`] with the Animation effects preference as given (`false` also when the
|
||||
/// system asks for no animations).
|
||||
fn presenter_frame_animating(ctx: &egui::Context, v: &mut DocView, info: &DocInfo, now: f64, animates: bool) -> bool {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::background());
|
||||
presenter::frame(v, info, &painter, ui.ctx(), now, animates, None);
|
||||
});
|
||||
// No renderer here to upload textures: drop the frame's texture changes.
|
||||
out.textures_delta.clear();
|
||||
out.viewport_output.get(&egui::ViewportId::ROOT).is_some_and(|o| o.repaint_delay.is_zero())
|
||||
}
|
||||
|
||||
/// A transition into a page the renderer could not draw ends at once: the page before is not held
|
||||
/// on screen waiting for it, and the page's own time can start.
|
||||
#[test]
|
||||
fn a_transition_into_a_page_that_failed_to_render_ends_at_once() {
|
||||
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)))];
|
||||
v.errors.insert(1, "the renderer failed".into());
|
||||
presenter_frame(&ctx, &mut v, &info, 0.1);
|
||||
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]
|
||||
fn a_display_time_on_the_last_spread_does_not_redraw_every_frame() {
|
||||
use pdfcraft_render::PageTransition;
|
||||
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 timed = PageTransition {
|
||||
page: 2,
|
||||
style: None,
|
||||
duration: 1.0,
|
||||
horizontal: false,
|
||||
inward: false,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display: Some(1.0),
|
||||
};
|
||||
let info = DocInfo { pages: vec![page.clone(), page.clone(), page.clone(), page], transitions: vec![timed], ..Default::default() };
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { layout: PageLayout::TwoUp, continuous: false, ..Default::default() });
|
||||
v.enter_presentation();
|
||||
v.current = 2;
|
||||
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(2, super::cache::PageTex { tag: 0, tex });
|
||||
// egui asks for its first frames itself: past them, only the presenter asks.
|
||||
for now in [0.0, 0.5, 0.6] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
let again = presenter_frame(&ctx, &mut v, &info, 5.0);
|
||||
assert_eq!(v.current, 2, "the last spread stays");
|
||||
assert!(!again, "and asks for no frame after frame");
|
||||
}
|
||||
|
||||
/// A page's display time (ISO 32000-2 §12.4.4.1, `/Dur`) counts from when the page is on screen:
|
||||
/// a slow render that still shows Rendering… does not use it up.
|
||||
#[test]
|
||||
fn a_display_time_starts_once_the_page_has_rendered() {
|
||||
use pdfcraft_render::PageTransition;
|
||||
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 timed = PageTransition {
|
||||
page: 0,
|
||||
style: None,
|
||||
duration: 1.0,
|
||||
horizontal: false,
|
||||
inward: false,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display: Some(1.0),
|
||||
};
|
||||
let info = DocInfo { pages: vec![page.clone(), page], transitions: vec![timed], ..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();
|
||||
for now in [0.0, 1.0, 2.0, 5.0] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
assert_eq!(v.current, 0, "page 1 is still rendering: its second has not started");
|
||||
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, 5.5);
|
||||
presenter_frame(&ctx, &mut v, &info, 6.0);
|
||||
assert_eq!(v.current, 0, "half of its second on screen");
|
||||
presenter_frame(&ctx, &mut v, &info, 6.6);
|
||||
assert_eq!(v.current, 1, "its second on screen is up");
|
||||
}
|
||||
|
||||
/// In two-page view a display time counts from when the whole spread is on screen: one page of
|
||||
/// it still showing Rendering… holds the spread (#5316 review).
|
||||
#[test]
|
||||
fn a_display_time_in_two_page_view_waits_for_the_whole_spread() {
|
||||
use pdfcraft_render::PageTransition;
|
||||
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 timed = PageTransition {
|
||||
page: 0,
|
||||
style: None,
|
||||
duration: 1.0,
|
||||
horizontal: false,
|
||||
inward: false,
|
||||
direction: Some(0),
|
||||
scale: 1.0,
|
||||
opaque: false,
|
||||
display: Some(1.0),
|
||||
};
|
||||
let info = DocInfo { pages: vec![page.clone(), page.clone(), page.clone(), page], transitions: vec![timed], ..Default::default() };
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { layout: PageLayout::TwoUp, continuous: false, ..Default::default() });
|
||||
v.enter_presentation();
|
||||
let ctx = egui::Context::default();
|
||||
let tex = |ctx: &egui::Context| ctx.load_texture("page", egui::ColorImage::new([4, 4], vec![Color32::WHITE; 16]), Default::default());
|
||||
let r = Rect::from_min_size(Pos2::ZERO, egui::vec2(300.0, 400.0));
|
||||
v.screen_rects = vec![(0, r), (1, r.translate(egui::vec2(300.0, 0.0)))];
|
||||
v.pages.insert(0, super::cache::PageTex { tag: 0, tex: tex(&ctx) });
|
||||
for now in [0.0, 1.0, 2.0, 5.0] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
assert_eq!(v.current, 0, "page 2 of the spread is still rendering: the spread's second has not started");
|
||||
v.pages.insert(1, super::cache::PageTex { tag: 0, tex: tex(&ctx) });
|
||||
presenter_frame(&ctx, &mut v, &info, 5.5);
|
||||
presenter_frame(&ctx, &mut v, &info, 6.0);
|
||||
assert_eq!(v.current, 0, "half of its second on screen");
|
||||
presenter_frame(&ctx, &mut v, &info, 6.6);
|
||||
assert_eq!(v.current, 2, "its second on screen is up: the next spread");
|
||||
}
|
||||
|
||||
/// Page 1 of `pages` 300×400 pt pages, with `transitions`, in full screen (single page), its
|
||||
/// raster in. The other pages get theirs with [`give_raster`].
|
||||
fn presenting_view(pages: usize, transitions: Vec<pdfcraft_render::PageTransition>, ctx: &egui::Context) -> (DocView, DocInfo) {
|
||||
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 info = DocInfo { pages: vec![page; pages], transitions, ..Default::default() };
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { layout: PageLayout::Single, continuous: false, ..Default::default() });
|
||||
v.enter_presentation();
|
||||
give_raster(ctx, &mut v, 0);
|
||||
(v, info)
|
||||
}
|
||||
|
||||
/// Page `page`'s raster has arrived (a blank one).
|
||||
fn give_raster(ctx: &egui::Context, v: &mut DocView, page: usize) {
|
||||
let tex = ctx.load_texture("page", egui::ColorImage::new([4, 4], vec![Color32::WHITE; 16]), Default::default());
|
||||
v.pages.insert(page, super::cache::PageTex { tag: 0, tex });
|
||||
}
|
||||
|
||||
/// Turn to `page` as a key would, with its raster in and its place on screen.
|
||||
fn turn_to(ctx: &egui::Context, v: &mut DocView, page: usize) {
|
||||
give_raster(ctx, v, page);
|
||||
v.current = page;
|
||||
v.screen_rects = vec![(page, Rect::from_min_size(Pos2::ZERO, egui::vec2(300.0, 400.0)))];
|
||||
}
|
||||
|
||||
/// A page's `/Trans` (style and `/D`) and `/Dur`, the rest as the spec's defaults.
|
||||
fn trans(page: usize, style: Option<pdfcraft_render::TransitionStyle>, duration: f32, display: Option<f32>) -> pdfcraft_render::PageTransition {
|
||||
pdfcraft_render::PageTransition { page, style, duration, horizontal: true, inward: true, direction: Some(0), scale: 1.0, opaque: false, display }
|
||||
}
|
||||
|
||||
/// A page with a transition but no display time (ISO 32000-2 Table 31, `Dur`: "By default, the
|
||||
/// viewer shall not advance automatically") stays until the user turns it, however long it shows.
|
||||
#[test]
|
||||
fn a_page_without_a_display_time_never_advances_by_itself() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(0, Some(TransitionStyle::Wipe), 1.0, None)], &ctx);
|
||||
for now in [0.0, 1.0, 60.0, 86_400.0, 1.0e7] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
assert_eq!(v.current, 0, "no /Dur: the page waits for the presenter");
|
||||
}
|
||||
|
||||
/// A display time of 0 seconds advances as soon as the page is on screen: zero is a number the
|
||||
/// spec allows, the time a page "shall be displayed" at most.
|
||||
#[test]
|
||||
fn a_display_time_of_zero_advances_once_the_page_is_on_screen() {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(0, None, 1.0, Some(0.0))], &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
assert_eq!(v.current, 1, "page 1's zero seconds are up at once");
|
||||
presenter_frame(&ctx, &mut v, &info, 0.1);
|
||||
assert_eq!(v.current, 1, "page 2 has no display time");
|
||||
}
|
||||
|
||||
/// A display time from the file that is negative, not a number or endless is no display time, or
|
||||
/// at most a day: it neither turns the page at once nor reaches a timer that can't hold it.
|
||||
#[test]
|
||||
fn a_negative_or_endless_display_time_is_no_display_time() {
|
||||
for display in [-1.0, -0.001, f32::NAN, f32::NEG_INFINITY, f32::INFINITY] {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(0, None, 1.0, Some(display))], &ctx);
|
||||
for now in [0.0, 0.1, 5.0, 3600.0] {
|
||||
presenter_frame(&ctx, &mut v, &info, now);
|
||||
}
|
||||
assert_eq!(v.current, 0, "/Dur {display}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A display time longer than a day is cut to a day; the largest number the file can hold would
|
||||
/// otherwise be past what a timer can wait for.
|
||||
#[test]
|
||||
fn a_display_time_longer_than_a_day_is_cut_to_a_day() {
|
||||
for display in [1.0e9, f32::MAX] {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(0, None, 1.0, Some(display))], &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
presenter_frame(&ctx, &mut v, &info, 86_399.0);
|
||||
assert_eq!(v.current, 0, "/Dur {display}: not yet a day");
|
||||
presenter_frame(&ctx, &mut v, &info, 86_400.5);
|
||||
assert_eq!(v.current, 1, "/Dur {display}: a day is up");
|
||||
}
|
||||
}
|
||||
|
||||
/// A transition the file says lasts a day (or longer) ends within ten seconds: it can't hold the
|
||||
/// screen.
|
||||
#[test]
|
||||
fn a_transition_of_a_day_ends_within_ten_seconds() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
for duration in [86_400.0, f32::MAX] {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(1, Some(TransitionStyle::Wipe), duration, None)], &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
turn_to(&ctx, &mut v, 1);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.1);
|
||||
presenter_frame(&ctx, &mut v, &info, 5.0);
|
||||
assert!(v.presenting_a_transition(), "/D {duration}: playing");
|
||||
presenter_frame(&ctx, &mut v, &info, 10.2);
|
||||
assert!(!v.presenting_a_transition(), "/D {duration}: over after ten seconds");
|
||||
}
|
||||
}
|
||||
|
||||
/// A transition of no length, a negative one or one that is not a number changes the page at once
|
||||
/// (a negative one would otherwise never end).
|
||||
#[test]
|
||||
fn a_transition_of_zero_or_negative_length_cuts() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
for duration in [0.0, -1.0, f32::NAN, f32::NEG_INFINITY] {
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(1, Some(TransitionStyle::Wipe), duration, None)], &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
turn_to(&ctx, &mut v, 1);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.1);
|
||||
assert!(!v.presenting_a_transition(), "/D {duration}: cut");
|
||||
assert_eq!(v.current, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// A style the spec doesn't name is read as its default R, Replace (ISO 32000-2 Table 164: "The
|
||||
/// new page simply replaces the old one with no special transition effect"): the page changes at
|
||||
/// once, and its display time still counts.
|
||||
#[test]
|
||||
fn an_unknown_transition_style_cuts_and_keeps_the_display_time() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
let ctx = egui::Context::default();
|
||||
let unknown = TransitionStyle::from_name("Spiral");
|
||||
let (mut v, info) = presenting_view(3, vec![trans(1, Some(unknown), 5.0, Some(1.0))], &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
turn_to(&ctx, &mut v, 1);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.1);
|
||||
assert!(!v.presenting_a_transition(), "no effect for an unknown style");
|
||||
presenter_frame(&ctx, &mut v, &info, 0.9);
|
||||
assert_eq!(v.current, 1, "its display time is not up");
|
||||
presenter_frame(&ctx, &mut v, &info, 1.2);
|
||||
assert_eq!(v.current, 2, "its display time is up");
|
||||
}
|
||||
|
||||
/// With animations off (the preference, or the system asking for none), a transition is a cut and
|
||||
/// the page's display time still counts from when it is on screen.
|
||||
#[test]
|
||||
fn without_animations_a_transition_cuts_and_the_display_time_counts() {
|
||||
use pdfcraft_render::TransitionStyle;
|
||||
let ctx = egui::Context::default();
|
||||
let (mut v, info) = presenting_view(3, vec![trans(1, Some(TransitionStyle::Wipe), 5.0, Some(1.0))], &ctx);
|
||||
presenter_frame_animating(&ctx, &mut v, &info, 0.0, false);
|
||||
turn_to(&ctx, &mut v, 1);
|
||||
presenter_frame_animating(&ctx, &mut v, &info, 0.1, false);
|
||||
assert!(!v.presenting_a_transition(), "cut");
|
||||
presenter_frame_animating(&ctx, &mut v, &info, 1.2, false);
|
||||
assert_eq!(v.current, 2, "page 2's second is up");
|
||||
}
|
||||
|
||||
/// A page turned by hand while a display time counts starts its own time afresh (ISO 32000-2
|
||||
/// §12.4.4.1, NOTE 1: "The user can advance the page manually before the specified time has
|
||||
/// expired"); the time left on the page before is not carried over.
|
||||
#[test]
|
||||
fn a_page_turned_by_hand_starts_its_own_display_time() {
|
||||
let ctx = egui::Context::default();
|
||||
let timed = (0..4).map(|p| trans(p, None, 1.0, Some(2.0))).collect();
|
||||
let (mut v, info) = presenting_view(4, timed, &ctx);
|
||||
presenter_frame(&ctx, &mut v, &info, 0.0);
|
||||
presenter_frame(&ctx, &mut v, &info, 1.5);
|
||||
turn_to(&ctx, &mut v, 1);
|
||||
presenter_frame(&ctx, &mut v, &info, 1.5);
|
||||
presenter_frame(&ctx, &mut v, &info, 3.0);
|
||||
assert_eq!(v.current, 1, "page 2 has shown 1.5 of its 2 seconds");
|
||||
presenter_frame(&ctx, &mut v, &info, 3.6);
|
||||
assert_eq!(v.current, 2, "page 2's own 2 seconds are up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_handles_account_for_document_and_view_rotation() {
|
||||
for page_rotation in [0, 90, 180, 270] {
|
||||
|
||||
@@ -6,6 +6,12 @@ impl DocView {
|
||||
self.screen_xforms.iter().find(|(p, _)| *p == page).map(|(_, xf)| xf.rect)
|
||||
}
|
||||
|
||||
/// Whether the page full screen shows is wholly in view: no transition playing or waiting for
|
||||
/// its raster, so a click on it reaches what is on it. Always true outside full screen.
|
||||
pub fn presentation_settled(&self) -> bool {
|
||||
self.presentation.is_none() || self.presenter.settled_on(self.current)
|
||||
}
|
||||
|
||||
/// The document area on screen.
|
||||
pub fn viewport_rect(&self) -> Rect {
|
||||
self.viewport_screen
|
||||
|
||||
@@ -35,12 +35,14 @@ impl DocView {
|
||||
self.continuous = false;
|
||||
self.fit = Fit::Page;
|
||||
self.selection = None;
|
||||
self.presenter = Default::default();
|
||||
self.goto = Some((self.current, 0.0));
|
||||
}
|
||||
|
||||
/// Leave full screen: the display from before it comes back, at the page reached meanwhile.
|
||||
pub fn leave_presentation(&mut self) {
|
||||
let Some(saved) = self.presentation.take() else { return };
|
||||
self.presenter = Default::default();
|
||||
self.layout = saved.layout;
|
||||
self.continuous = saved.continuous;
|
||||
self.cover = saved.cover;
|
||||
@@ -65,6 +67,12 @@ impl DocView {
|
||||
self.presentation.is_some()
|
||||
}
|
||||
|
||||
/// Whether full screen is playing a page transition (or holding the page before it until the
|
||||
/// next one has rendered).
|
||||
pub fn presenting_a_transition(&self) -> bool {
|
||||
self.presenter.turning()
|
||||
}
|
||||
|
||||
/// Switch the base page display (Single or Two-page), leaving continuous scrolling as it is.
|
||||
/// A no-op when the layout is already current, so re-selecting it never yanks a scrolled view
|
||||
/// back to the top.
|
||||
|
||||
@@ -2631,6 +2631,547 @@ fn a_document_press_releases_pages_keyboard_focus_without_escape() {
|
||||
assert!(h.state().views[0].selected.is_empty(), "Ctrl+A now belongs to the viewer, not page thumbnails");
|
||||
}
|
||||
|
||||
/// Three 300×400 pt pages; the second comes in with a one-second Wipe and stays one second
|
||||
/// (`/Trans`, `/Dur`), the others have neither.
|
||||
const PRESENTATION: &[u8] = b"%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 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R /Trans << /S /Wipe /D 1 >> /Dur 1 >> endobj
|
||||
5 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
/// The presentation document in full screen, frames a tenth of a second apart, every raster in.
|
||||
fn presenting(options: &'static [(&'static str, &'static str)]) -> Harness<'static, PdfCraftApp> {
|
||||
presenting_pdf(PRESENTATION, options)
|
||||
}
|
||||
|
||||
/// [`presenting`] with another document.
|
||||
fn presenting_pdf(pdf: &'static [u8], options: &'static [(&'static str, &'static str)]) -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(0.1).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("slides.pdf", None, pdf.to_vec()).expect("opens");
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.set_option("panel", "none").unwrap();
|
||||
app.set_option("animation_effects", "on").unwrap();
|
||||
for (k, v) in options {
|
||||
app.set_option(k, v).unwrap();
|
||||
}
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(4);
|
||||
assert!(h.state().full_screen);
|
||||
settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
/// Run frames until the renderer has nothing left to deliver (bounded, in real time: the
|
||||
/// renderer's threads do not follow the harness clock).
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..300 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
/// Run frames until `done` holds (bounded), returning the number of frames it took.
|
||||
fn frames_until(h: &mut Harness<'static, PdfCraftApp>, done: impl Fn(&PdfCraftApp) -> bool) -> usize {
|
||||
for n in 0..300 {
|
||||
if done(h.state()) {
|
||||
return n;
|
||||
}
|
||||
h.run_steps(1);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("never happened");
|
||||
}
|
||||
|
||||
/// #379: in full screen, a page's /Trans plays when it comes into view (from the page before,
|
||||
/// once the new one has rendered), and its /Dur turns to the next page once it is up; the last
|
||||
/// page, with no display time, stays.
|
||||
#[test]
|
||||
fn full_screen_plays_a_page_transition_and_honours_the_display_time() {
|
||||
let mut h = presenting(&[]);
|
||||
assert!(!h.state().views[0].presenting_a_transition(), "page 1 just shows");
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "page 2 comes in with its Wipe");
|
||||
// The Wipe is painted, not only timed: once page 2 has rendered, a frame shows a strip of it as
|
||||
// tall as the page and only part of its width.
|
||||
let wipe_strip = |h: &Harness<'static, PdfCraftApp>| {
|
||||
h.state().views[0].page_screen_rect(1).is_some_and(|page| {
|
||||
h.output().shapes.iter().any(|clipped| {
|
||||
let egui::Shape::Mesh(mesh) = &clipped.shape else { return false };
|
||||
let b = mesh.calc_bounds();
|
||||
mesh.texture_id != egui::TextureId::default()
|
||||
&& (b.min.x - page.min.x).abs() < 1.0
|
||||
&& (b.min.y - page.min.y).abs() < 1.0
|
||||
&& (b.max.y - page.max.y).abs() < 1.0
|
||||
&& b.width() > 1.0
|
||||
&& b.width() < page.width() - 1.0
|
||||
})
|
||||
})
|
||||
};
|
||||
let mut steps = 0;
|
||||
while !wipe_strip(&h) && steps < 300 {
|
||||
h.run_steps(1);
|
||||
steps += 1;
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
assert!(wipe_strip(&h), "the Wipe paints part of page 2 ({steps} frames)");
|
||||
// Counted from the first painted strip, when page 2's raster is in and the Wipe is under way:
|
||||
// the frames spent waiting for the raster don't count toward its second.
|
||||
let frames = frames_until(&mut h, |a| !a.views[0].presenting_a_transition());
|
||||
assert!(frames >= 8, "the Wipe takes its second ({frames} frames of 0.1 s after the first strip)");
|
||||
assert_eq!(h.state().views[0].current, 1, "still page 2 when the transition ends");
|
||||
h.run_steps(5);
|
||||
assert_eq!(h.state().views[0].current, 1, "half of its display time");
|
||||
h.run_steps(7);
|
||||
assert_eq!(h.state().views[0].current, 2, "its one-second display time is up");
|
||||
assert!(!h.state().views[0].presenting_a_transition(), "page 3 has no transition");
|
||||
h.run_steps(30);
|
||||
assert_eq!(h.state().views[0].current, 2, "the last page stays");
|
||||
h.key_press(Key::Escape);
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().views[0].presenting_a_transition());
|
||||
}
|
||||
|
||||
/// The page changes at once where a transition can't or shouldn't play: with the Animation
|
||||
/// effects preference off, and with two-page spreads.
|
||||
#[test]
|
||||
fn full_screen_turns_at_once_without_animations_or_in_spreads() {
|
||||
for options in [&[("animation_effects", "off")][..], &[("layout", "two-up")][..]] {
|
||||
let mut h = presenting(options);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert!(!h.state().views[0].presenting_a_transition(), "{options:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// In full screen the pointer hides after three still seconds, and shows again when it moves.
|
||||
#[test]
|
||||
fn full_screen_hides_a_still_pointer() {
|
||||
let mut h = presenting(&[]);
|
||||
let centre = h.state().views[0].viewport_rect().center();
|
||||
h.hover_at(centre);
|
||||
h.run_steps(2);
|
||||
assert_ne!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "just moved");
|
||||
h.run_steps(32);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "still for over three seconds");
|
||||
h.hover_at(centre + egui::vec2(40.0, 0.0));
|
||||
h.run_steps(1);
|
||||
assert_ne!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "moved again");
|
||||
}
|
||||
|
||||
/// In full screen a right-click goes back a page, as a left click goes forward (*Adobe Acrobat
|
||||
/// Help*, 2019, "Full Screen preferences", p. 69: "Left Click To Go Forward One Page; Right Click
|
||||
/// To Go Back One Page"), and no context menu opens.
|
||||
#[test]
|
||||
fn full_screen_right_click_goes_back_a_page() {
|
||||
// A document with no display times, so no page turns by itself meanwhile.
|
||||
let mut h = harness(&[]);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
assert!(h.state().full_screen);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
let centre = h.state().views[0].viewport_rect().center();
|
||||
h.hover_at(centre);
|
||||
h.run_steps(1);
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: centre, button: egui::PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(6);
|
||||
assert_eq!(h.state().views[0].current, 0, "a right-click goes back");
|
||||
assert!(!egui::Popup::is_any_open(&h.ctx), "no context menu in full screen");
|
||||
}
|
||||
|
||||
/// In full screen Enter goes forward a page and Shift+Enter back (*Adobe Acrobat Help*, 2019,
|
||||
/// "Read a document in Full Screen mode", p. 66: "To go to the previous page, press Shift+Enter").
|
||||
#[test]
|
||||
fn full_screen_shift_enter_goes_back_a_page() {
|
||||
// A document with no display times, so no page turns by itself meanwhile.
|
||||
let mut h = harness(&[]);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
assert!(h.state().full_screen);
|
||||
h.key_press(Key::Enter);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, 1, "Enter goes forward");
|
||||
h.key_press_modifiers(Modifiers::SHIFT, Key::Enter);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, 0, "Shift+Enter goes back");
|
||||
}
|
||||
|
||||
/// A document replaced while full screen plays a transition (a reload) starts afresh: none of the
|
||||
/// old document's transition, page or timer carries over.
|
||||
#[test]
|
||||
fn full_screen_starts_afresh_when_the_document_is_replaced() {
|
||||
let mut h = presenting(&[]);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "page 2 comes in with its Wipe");
|
||||
{
|
||||
let app = h.state_mut();
|
||||
let info = app.session.get(app.views[0].id).unwrap().info.clone();
|
||||
app.views[0].document_replaced(&info);
|
||||
}
|
||||
assert!(!h.state().views[0].presenting_a_transition(), "the old transition is dropped");
|
||||
}
|
||||
|
||||
/// A still pointer hides over a link too: the page's own cursor (the hand over a link) is chosen
|
||||
/// first, and full screen hides the pointer after it.
|
||||
#[test]
|
||||
fn full_screen_hides_a_still_pointer_over_a_link() {
|
||||
let mut h = harness(&[]);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
assert!(h.state().full_screen);
|
||||
// Page 1's link covers x 50–250, y 300–350 of the 300×400 pt page (from its bottom).
|
||||
let r = rect(&h, 0).expect("page 1 on screen");
|
||||
let on_link = egui::pos2(r.left() + r.width() * 150.0 / 300.0, r.top() + r.height() * 75.0 / 400.0);
|
||||
h.hover_at(on_link);
|
||||
h.run_steps(1);
|
||||
assert_ne!(h.output().platform_output.cursor_icon, egui::CursorIcon::None, "just moved, over the 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;
|
||||
// Until page 1's raster is in, full screen veils it and a click turns the page; on a slow
|
||||
// machine that takes more frames than the six above.
|
||||
for _ in 0..600 {
|
||||
if h.state().views[n].presentation_settled() {
|
||||
break;
|
||||
}
|
||||
h.run_steps(1);
|
||||
}
|
||||
assert!(h.state().views[n].presentation_settled(), "page 1 is shown");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Two pages, the second with a text field and a five-second Wipe.
|
||||
const FIELD_BEHIND_TRANSITION: &[u8] = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 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 >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R /Annots [5 0 R] /Trans << /S /Wipe /D 5 >> >> endobj
|
||||
5 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [50 100 250 140] /P 4 0 R /F 4 >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
/// #5316 review: while full screen turns to a page (its transition playing, or its raster on the
|
||||
/// way), the page isn't the one on screen yet, so a click there opens none of its fields; once
|
||||
/// the page is in, the same click does.
|
||||
#[test]
|
||||
fn full_screen_takes_no_page_input_until_the_page_is_on_screen() {
|
||||
let mut h = presenting_pdf(FIELD_BEHIND_TRANSITION, &[]);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "page 2 comes in with its Wipe");
|
||||
let r = h.state().views[0].page_screen_rect(1).expect("page 2 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(2);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "the Wipe is still playing");
|
||||
assert!(!h.ctx.egui_wants_keyboard_input(), "the field behind the transition isn't opened");
|
||||
frames_until(&mut h, |a| !a.views[0].presenting_a_transition());
|
||||
h.run_steps(1);
|
||||
click_with(&mut h, on_field, Modifiers::NONE);
|
||||
h.run_steps(4);
|
||||
assert!(h.ctx.egui_wants_keyboard_input(), "once page 2 is in, the field opens");
|
||||
}
|
||||
|
||||
/// [`FIELD_BEHIND_TRANSITION`] with a Wipe of 0.3 s, shorter than a click may be held.
|
||||
const FIELD_BEHIND_SHORT_TRANSITION: &[u8] = b"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 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 >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R /Annots [5 0 R] /Trans << /S /Wipe /D 0.3 >> >> endobj
|
||||
5 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [50 100 250 140] /P 4 0 R /F 4 >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
|
||||
/// #5316 review: a press made while full screen turns to a page belongs to nothing on it, through
|
||||
/// its release: let go on the field just after the transition ends, within a click's time, it
|
||||
/// still opens nothing.
|
||||
#[test]
|
||||
fn full_screen_press_made_during_a_transition_opens_nothing_when_released() {
|
||||
let mut h = presenting_pdf(FIELD_BEHIND_SHORT_TRANSITION, &[]);
|
||||
let r = h.state().views[0].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);
|
||||
h.hover_at(on_field);
|
||||
h.run_steps(1);
|
||||
// Page 2 renders ahead: once nothing is left to render, its Wipe starts with the key.
|
||||
settle(&mut h);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "page 2 comes in with its Wipe");
|
||||
h.event(egui::Event::PointerButton { pos: on_field, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.run_steps(1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "pressed while the Wipe plays");
|
||||
let frames = frames_until(&mut h, |a| !a.views[0].presenting_a_transition());
|
||||
assert!(frames <= 4, "the Wipe ends within a click's time ({frames} frames of 0.1 s)");
|
||||
h.event(egui::Event::PointerButton { pos: on_field, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(4);
|
||||
assert!(!h.ctx.egui_wants_keyboard_input(), "released on the field once page 2 is in: nothing opens");
|
||||
}
|
||||
|
||||
/// 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);
|
||||
// Until the page's raster is in: before it, full screen takes no press on the page.
|
||||
settle(&mut h);
|
||||
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");
|
||||
}
|
||||
|
||||
/// With Animation effects following the system and the system asking for no animations (Windows'
|
||||
/// Animation effects off, or another system's Reduce motion), full screen changes pages at once
|
||||
/// and still keeps the page's display time; with the system not asking, the same transition plays.
|
||||
#[test]
|
||||
fn full_screen_cuts_transitions_when_the_system_asks_for_no_animations() {
|
||||
for reduced in [true, false] {
|
||||
let mut h = presenting(&[("animation_effects", "system")]);
|
||||
h.state_mut().motion.reduced = reduced;
|
||||
h.run_steps(1);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
assert_eq!(h.state().views[0].presenting_a_transition(), !reduced, "system asks for no animations: {reduced}");
|
||||
if reduced {
|
||||
settle(&mut h);
|
||||
let frames = frames_until(&mut h, |a| a.views[0].current == 2);
|
||||
assert!(frames <= 20, "page 2's one-second display time still turns it ({frames} frames of 0.1 s)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape leaves full screen while a transition plays and while a display time counts, and the
|
||||
/// display time stops with it: the page reached stays.
|
||||
#[test]
|
||||
fn escape_leaves_full_screen_mid_transition_and_stops_the_display_time() {
|
||||
for mid_transition in [true, false] {
|
||||
let mut h = presenting(&[]);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert!(h.state().views[0].presenting_a_transition(), "page 2 comes in with its Wipe");
|
||||
if !mid_transition {
|
||||
frames_until(&mut h, |a| !a.views[0].presenting_a_transition());
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].current, 1, "page 2's display time is counting");
|
||||
}
|
||||
h.key_press(Key::Escape);
|
||||
h.run_steps(1);
|
||||
assert!(!h.state().full_screen, "Escape leaves full screen (mid-transition: {mid_transition})");
|
||||
assert!(!h.state().views[0].presenting_a_transition());
|
||||
h.run_steps(30);
|
||||
assert_eq!(h.state().views[0].current, 1, "no page turns by itself after leaving (mid-transition: {mid_transition})");
|
||||
}
|
||||
}
|
||||
|
||||
/// While a page's display time counts, the keys still turn pages at once (ISO 32000-2 §12.4.4.1,
|
||||
/// NOTE 1: "The user can advance the page manually before the specified time has expired"):
|
||||
/// → goes on before the time is up, and ← back to a page with no display time, which then stays.
|
||||
#[test]
|
||||
fn full_screen_keys_take_over_from_a_display_time() {
|
||||
let mut h = presenting(&[("animation_effects", "off")]);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 1, "page 2, with its one-second display time");
|
||||
h.key_press(Key::ArrowLeft);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 0, "← goes back before the time is up");
|
||||
h.run_steps(30);
|
||||
assert_eq!(h.state().views[0].current, 0, "page 1 has no display time: it stays");
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(2);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 2, "→ goes on before page 2's time is up");
|
||||
}
|
||||
|
||||
/// Every way full screen moves: forward (→, Space, Enter, Page Down, a click), back (←, Backspace,
|
||||
/// Shift+Enter, Page Up, a right-click, a Shift-click), first (Home, Shift+Ctrl+Page Up), last (End,
|
||||
/// Shift+Ctrl+Page Down) and out (Escape, Ctrl+L).
|
||||
#[test]
|
||||
fn full_screen_every_key_and_click_goes_where_it_says() {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Input {
|
||||
Key(Key, Modifiers),
|
||||
Click(egui::PointerButton, Modifiers),
|
||||
}
|
||||
use Input::{Click, Key as K};
|
||||
use egui::PointerButton::{Primary, Secondary};
|
||||
let both = Modifiers::COMMAND | Modifiers::SHIFT;
|
||||
// (input, page before, page after); a page past the end stays where it is.
|
||||
let moves = [
|
||||
(K(Key::ArrowRight, Modifiers::NONE), 0, 1),
|
||||
(K(Key::Space, Modifiers::NONE), 0, 1),
|
||||
(K(Key::Enter, Modifiers::NONE), 0, 1),
|
||||
(K(Key::PageDown, Modifiers::NONE), 0, 1),
|
||||
(Click(Primary, Modifiers::NONE), 0, 1),
|
||||
(K(Key::ArrowLeft, Modifiers::NONE), 1, 0),
|
||||
(K(Key::Backspace, Modifiers::NONE), 1, 0),
|
||||
(K(Key::Enter, Modifiers::SHIFT), 1, 0),
|
||||
(K(Key::PageUp, Modifiers::NONE), 1, 0),
|
||||
(Click(Secondary, Modifiers::NONE), 1, 0),
|
||||
(Click(Primary, Modifiers::SHIFT), 1, 0),
|
||||
(K(Key::Home, Modifiers::NONE), 2, 0),
|
||||
(K(Key::PageUp, both), 2, 0),
|
||||
(K(Key::End, Modifiers::NONE), 0, 2),
|
||||
(K(Key::PageDown, both), 0, 2),
|
||||
];
|
||||
// A document with no display times, so no page turns by itself meanwhile.
|
||||
let mut h = harness(&[]);
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
assert!(h.state().full_screen);
|
||||
for (input, from, to) in moves {
|
||||
h.state_mut().views[0].go_to_page(from);
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().views[0].current, from, "{input:?}: on page {}", from + 1);
|
||||
match input {
|
||||
K(key, modifiers) => {
|
||||
h.key_press_modifiers(modifiers, key);
|
||||
h.run_steps(4);
|
||||
}
|
||||
Click(button, modifiers) => {
|
||||
// Clear of page 1's link, which a click would follow instead.
|
||||
let r = rect(&h, from).expect("the page is on screen");
|
||||
let at = egui::pos2(r.center().x, r.bottom() - 20.0);
|
||||
h.hover_at(at);
|
||||
h.run_steps(1);
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos: at, button, pressed, modifiers });
|
||||
}
|
||||
h.run_steps(6);
|
||||
}
|
||||
}
|
||||
assert_eq!(h.state().views[0].current, to, "{input:?} from page {}", from + 1);
|
||||
assert!(h.state().full_screen, "{input:?} stays in full screen");
|
||||
}
|
||||
for (key, modifiers) in [(Key::Escape, Modifiers::NONE), (Key::L, Modifiers::COMMAND)] {
|
||||
if !h.state().full_screen {
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::L);
|
||||
h.run_steps(6);
|
||||
}
|
||||
assert!(h.state().full_screen);
|
||||
h.key_press_modifiers(modifiers, key);
|
||||
h.run_steps(4);
|
||||
assert!(!h.state().full_screen, "{key:?} leaves full screen");
|
||||
}
|
||||
}
|
||||
|
||||
/// A transition style the spec doesn't name (here `/Spiral`, with a five-second `/D`) is the
|
||||
/// default, Replace (ISO 32000-2 Table 164, `S`: "Default value: R"): the page changes at once, and
|
||||
/// its display time still turns it.
|
||||
#[test]
|
||||
fn full_screen_cuts_an_unknown_transition_style_and_keeps_its_display_time() {
|
||||
const SPIRAL: &[u8] = b"%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 5 0 R] /Count 3 /MediaBox [0 0 300 400] >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
4 0 obj << /Type /Page /Parent 2 0 R /Trans << /S /Spiral /D 5 >> /Dur 1 >> endobj
|
||||
5 0 obj << /Type /Page /Parent 2 0 R >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
let mut h = presenting_pdf(SPIRAL, &[]);
|
||||
h.key_press(Key::ArrowRight);
|
||||
h.run_steps(1);
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
assert!(!h.state().views[0].presenting_a_transition(), "no effect for an unknown style");
|
||||
settle(&mut h);
|
||||
let frames = frames_until(&mut h, |a| a.views[0].current == 2);
|
||||
assert!(frames <= 20, "its one-second display time turns it ({frames} frames of 0.1 s)");
|
||||
}
|
||||
|
||||
/// A one-page PDF whose content inflates past the renderer's per-stream limit (#623): a window
|
||||
/// of whitespace the interpreter can never complete, so the page is cut and says so.
|
||||
fn cut_content_pdf() -> Vec<u8> {
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-24 · **Change:** minor (→ / ← scroll a page wider than the window before turning it; a comment's text, border or line, and fill each at an opacity of its own) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-29 · **Last updated:** 2026-10-29 · **Change:** minor (full screen plays page transitions and display times, hides a still pointer, and a right-click or Shift+Enter goes back a page #379) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
|
||||
How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the
|
||||
2023+ "new Acrobat": All tools pane on the left, quick-action toolbar, Home view). Behaviour is
|
||||
@@ -33,7 +33,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
|
||||
| Document tabs | yes | yes, with unsaved marker | shipped |
|
||||
| Multiple windows, drag a tab out | yes | no | planned |
|
||||
| Split window, spreadsheet split | yes | split right or down (⌘\\, tab menu, a tab dropped on the page), one document or two; no four-pane spreadsheet split | split shipped; spreadsheet split planned |
|
||||
| Read mode, full screen | yes | yes; full screen is one fitted page at a time on black, click/keys turn pages (#1459) | shipped; auto-advance, loop and transitions planned |
|
||||
| Read mode, full screen | yes | yes; full screen is one fitted page or spread at a time on black, and background clicks and keys turn pages while clicks on links, comments, fields and other page objects do not (#1459), a right-click or Shift+Enter goes back, page transitions and display times play (#379), a still pointer hides | shipped; loop after the last page and the full-screen preferences planned |
|
||||
| Light, dark, system theme | yes | yes | shipped |
|
||||
| Side panels: thumbnails, bookmarks, attachments, layers, comments, fields, signatures | yes | yes; panels keep their width (#704); the Pages inspector has a 50–150% thumbnail size slider and buttons (#3808) | shipped |
|
||||
| Destinations, articles, model tree, Tags, Order, Content panels | yes | no | planned |
|
||||
@@ -86,6 +86,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-29 | minor | Full screen plays page transitions (/Trans) and display times (/Dur), hides a still pointer, and a right-click or Shift+Enter goes back a page (#379) |
|
||||
| 2026-10-24 | minor | → / ← scroll a page wider than the window sideways before turning it, by the current page's or spread's edges in continuous view; ⌘→ / ⌘← turn at once |
|
||||
| 2026-10-24 | minor | Comment Properties sets a text box's text, border and fill opacity and a shape's line and fill opacity each on its own (Acrobat: one per comment); Make Properties Default carries them into the tool |
|
||||
| 2026-10-21 | minor | Pages inspector thumbnail sizing (#3808), separate from page and Organize zoom; view setting retained on reload and exposed through UI control. Not persisted between sessions. |
|
||||
|
||||
@@ -1118,8 +1118,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["view.full_screen"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/it/commands.rs::full_screen_toggles_by_shortcut_and_command_and_escape_leaves", "crates/ui-egui/tests/it/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it", "crates/ui-egui/tests/it/view.rs::full_screen_keeps_two_page_view_as_spreads"]
|
||||
notes = "Acrobat's Full Screen Mode (#1459): the page on black and nothing else, one page at a time fitted to the screen (one spread when two-page view was on, #1353), from the current page; a click, Space, Enter, \u2192 or Page Down go forward, a shift-click, Backspace, \u2190 or Page Up back, Home/End to the ends; Esc or \u2318L bring the display from before back at the page reached. Links still open on click. Missing: the cursor is not hidden, no transitions, auto-advance or loop (view.full-screen-presentation), and no full-screen preferences such as the background colour"
|
||||
evidence = ["crates/ui-egui/tests/it/commands.rs::full_screen_toggles_by_shortcut_and_command_and_escape_leaves", "crates/ui-egui/tests/it/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it", "crates/ui-egui/tests/it/view.rs::full_screen_keeps_two_page_view_as_spreads", "crates/ui-egui/tests/it/view.rs::full_screen_right_click_goes_back_a_page", "crates/ui-egui/tests/it/view.rs::full_screen_shift_enter_goes_back_a_page", "crates/ui-egui/tests/it/view.rs::full_screen_hides_a_still_pointer"]
|
||||
notes = "Acrobat's Full Screen Mode (#1459): the page on black and nothing else, one page at a time fitted to the screen (one spread when two-page view was on, #1353), from the current page; a click, Space, Enter, \u2192 or Page Down go forward, a shift-click, a right-click (no context menu in full screen), Shift+Enter, Backspace, \u2190 or Page Up back, Home/End to the ends; Esc or \u2318L bring the display from before back at the page reached. Links still open on click. The pointer hides after three still seconds. Transitions and display times: view.full-screen-presentation. Missing: loop after the last page, and full-screen preferences such as the background colour"
|
||||
|
||||
[[feature]]
|
||||
id = "view.full-screen-presentation"
|
||||
@@ -1130,8 +1130,8 @@ milestone = "M12"
|
||||
status = "partial"
|
||||
commands = ["view.full_screen"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/it/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it"]
|
||||
notes = "Click to advance (and shift-click back) ships with view.full-screen; auto-advance every N seconds, loop after the last page, page transitions and the full-screen preferences are not done"
|
||||
evidence = ["crates/ui-egui/tests/it/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it", "crates/ui-egui/tests/it/view.rs::full_screen_plays_a_page_transition_and_honours_the_display_time", "crates/ui-egui/tests/it/view.rs::full_screen_turns_at_once_without_animations_or_in_spreads", "crates/ui-egui/tests/it/view.rs::full_screen_hides_a_still_pointer"]
|
||||
notes = "Click to advance (and shift-click back) ships with view.full-screen. A page's /Trans plays when it comes into view (ISO 32000-2 \u00a712.4.4, Table 164; single-page view, no view rotation; the outgoing page is held until the incoming one has rendered; the Animation effects preference turns them off), a page's /Dur advances to the next page once it is up (not past the last), and the pointer hides after three still seconds. Not done: full-screen preferences that override the document (advance every N seconds, loop after the last page, a default transition, ignoring the document's transitions, the pointer setting), and transitions between two-page spreads"
|
||||
|
||||
[[feature]]
|
||||
id = "view.reflow"
|
||||
@@ -3275,7 +3275,7 @@ status = "partial"
|
||||
commands = []
|
||||
tools = ["doc_info", "page_transition_set"]
|
||||
evidence = ["crates/render/src/inspect/tests.rs::page_transitions_and_display_times_are_read", "crates/automation/tests/it/automation.rs::doc_info_lists_page_transitions_and_display_times", "crates/organize/src/tests.rs::page_transitions_are_set_removed_and_checked", "crates/automation/tests/it/automation.rs::page_transitions_are_set_and_removed_through_the_tools"]
|
||||
notes = "Read and set, not played: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults and listed by doc_info (DocInfo::transitions); page_transition_set (Edit::SetTransition, one undo step) writes or removes them on chosen pages with the values checked first. Not yet: playing them in full screen, auto-advancing on /Dur, and the Page Transitions dialog"
|
||||
notes = "Read, set and played: each page's /Trans (style, duration, dimension, motion, direction, Fly scale and backdrop) and /Dur display time are read with the spec's defaults and listed by doc_info (DocInfo::transitions); page_transition_set (Edit::SetTransition, one undo step) writes or removes them on chosen pages with the values checked first. Full screen plays them and advances on /Dur (view.full-screen-presentation). Not yet: the Page Transitions dialog"
|
||||
|
||||
[[feature]]
|
||||
id = "organize.page-tab-order"
|
||||
|
||||
Reference in new issue
Block a user