Merge #4378: M3: documents that hide the toolbar, menu bar or window controls open that way, with a key to bring them back (organize.initial-view)

# Conflicts:
#	crates/engine/src/commands.rs
#	crates/ui-egui/src/commands.rs
This commit is contained in:
bflatastic committed 2026-11-09 18:23:10 +07:00
commit 6d1efb7646
15 files changed
+826 -12

No files matched your search

+10
View File
@@ -231,6 +231,16 @@ pub const COMMANDS: &[CommandSpec] = &[
// Split view (Acrobat's Window ▸ Split): the current document in two panes, or back to one;
// a tab's right-click menu also splits right or down with any tab.
c("view.split", "Split", VIEW, Some(Shortcut::cmd("\\")), Document, "columns-2"),
// A document can hide the toolbars, menu and panels (ISO 32000-2 Table 147); this brings them
// back for its tab, and with nothing hidden hides them.
c(
"view.toggle_interface",
"Show or hide toolbars and panels",
VIEW,
Some(Shortcut { command: false, shift: false, mac_ctrl: false, key: "F8" }),
Document,
"panel-left",
),
// Browsers' tab keys (#1542): Ctrl+Tab on every platform, ⌃Tab rather than ⌘Tab on macOS,
// where the system takes ⌘Tab. They follow the tabs' order on screen and wrap around, and
// also run from Combine files, which has no document open.
+13 -3
View File
@@ -305,6 +305,7 @@ impl PdfCraftApp {
community_card_closed: false,
pending_link: None,
pending_full_screen: None,
pending_window: None,
pending_remote_link: None,
protect_draft: Default::default(),
text_style_draft: Default::default(),
@@ -779,7 +780,12 @@ impl PdfCraftApp {
window_controls::resize_edges(&ctx);
}
chrome::tab_strip(self, ui);
chrome::mode_bar(self, ui);
// A document can hide the toolbars, the menu and the panels around it in its tab (ISO
// 32000-2 Table 147); the tab strip stays, and F8 brings the rest back (`viewer_prefs`).
let hidden = self.hidden_interface();
if !hidden.mode_bar(self.native_menu.is_some()) {
chrome::mode_bar(self, ui);
}
// Another mode or the Home tab puts the OCR Verify screen away, as Exit does; Correct
// recognized text brings it back with its review as it was.
if self.ocr_verify.open && (self.active.is_none() || self.ocr_verify.shown_in.is_some_and(|m| m != self.mode)) {
@@ -806,15 +812,19 @@ impl PdfCraftApp {
if self.all_tools_full && self.all_tools_tab != self.shown_tab() {
self.all_tools_full = false;
}
side_panels::show(self, ui);
if !hidden.window_ui {
side_panels::show(self, ui);
}
let t = theme::Tokens::get(&ctx);
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| {
if self.mode == Mode::AllTools && self.all_tools_full {
if self.mode == Mode::AllTools && self.all_tools_full && !hidden.window_ui {
all_tools_ui::show(self, ui);
} else {
panes::page_area(self, ui);
}
});
// FitWindow and CenterWindow, once the document just opened is laid out.
self.window_preferences_frame(&ctx);
self.process_pending_edits();
palette::show(self, &ctx);
dialogs::show(self, &ctx);
+4 -2
View File
@@ -351,9 +351,11 @@ impl PdfCraftApp {
self.active = Some(self.views.len() - 1);
}
self.apply_initial_view(self.views.len() - 1, &initial);
// Where the reader left off wins over the file's own initial view (#1767).
self.apply_viewer_preferences(self.views.len() - 1, &initial, activate);
// Where the reader left off wins over the file's own initial view and the zoom its
// viewer preferences open at (#1767).
self.apply_last_view(self.views.len() - 1);
// After both, so full screen shows one fitted page and leaving it comes back to them.
// After all of them, so full screen shows one fitted page and leaving it comes back to them.
if self.full_screen
&& let Some(view) = self.views.last_mut()
{
+13 -1
View File
@@ -561,6 +561,9 @@ pub struct DocView {
pub flash: Option<(usize, [f32; 4], f64)>,
/// Compare files: differences shaded on this document's pages (page, user-space box, colour).
pub compare_marks: Vec<(usize, [f32; 4], Color32)>,
/// The interface the document hid in this tab, and what is hidden now (ISO 32000-2 Table 147
/// `HideToolbar`, `HideMenubar`, `HideWindowUI`). Never saved.
pub interface: crate::viewer_prefs::TabInterface,
/// 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>,
@@ -1039,6 +1042,7 @@ impl DocView {
single_edges: None,
flash: None,
compare_marks: Vec::new(),
interface: Default::default(),
presentation: None,
pages: HashMap::new(),
errors: HashMap::new(),
@@ -1214,6 +1218,8 @@ impl DocView {
fresh.snapshot = self.snapshot.clone();
fresh.close_requested = self.close_requested;
fresh.cover = self.cover;
// What the document asked to hide, and what is hidden now (F8), belong to the tab.
fresh.interface = self.interface;
// Still presenting, with the display to go back to on leaving.
fresh.presentation = self.presentation.take();
fresh.back = std::mem::take(&mut self.back);
@@ -2609,6 +2615,11 @@ fn page_scroll_area(ui: &egui::Ui, view: &mut DocView, geometry: &ScrollGeometry
// Every scroll here asks for no animation. An animated area would still queue a zero-length
// offset target for the next frame, which overwrites the jump a page turn just set.
scroll = scroll.animated(false);
// A document that hides the window's interface (`HideWindowUI`) hides the scroll bars too;
// the wheel, keys and Hand tool still scroll.
if view.interface.hidden.window_ui {
scroll = scroll.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden);
}
// A newly opened document starts at the top. eframe saves egui's memory with the settings
// and document ids start again at 1 every launch, so the offset stored under this id can be
// where an earlier session left another document.
@@ -3721,7 +3732,8 @@ fn apply_frame_requests(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui,
}
// Read mode hides the tools (#2646), and full screen is the page and nothing else (#1459);
// the quick tools come back with the window.
if app.mode != crate::Mode::Read && !app.full_screen {
// A document that hides the toolbars (`HideToolbar`) hides this one too.
if app.mode != crate::Mode::Read && !app.full_screen && !app.views.get(index).is_some_and(|v| v.interface.hidden.toolbar) {
// The page nearest the left edge, whose margin the bar sits in.
let page_left = app.views[index].screen_rects.iter().map(|(_, r)| r.left()).reduce(f32::min);
quick_bar(app, avail, page_left, ui);
+29
View File
@@ -2695,6 +2695,35 @@ fn a_moved_picture_is_drawn_from_the_fresh_raster_while_a_tile_is_still_stale()
h.state_mut().views[0].tiles.clear();
}
/// Review on #4378: `FitWindow` measures the spread the view shows, its pages side by side
/// and as tall as the taller one, with one page in a spread that has only one.
#[test]
fn first_page_now_measures_the_pages_in_the_spread() {
let page =
|width: f32, height: f32| pdfcraft_render::PageInfo { width, height, label: String::new(), crop: [0.0, 0.0, width, height], rotation: 0 };
let info = DocInfo { pages: vec![page(300.0, 400.0), page(600.0, 800.0), page(200.0, 500.0)], ..Default::default() };
let mut v = DocView::new(DocId(1), &info, ViewDefaults::default());
let one = v.first_page_now(&info).expect("a page");
assert_eq!((one.size, one.margins.x), (vec2(300.0 * PT, 400.0 * PT), 2.0 * SIDE));
v.set_display(PageLayout::TwoUp, false);
let gap = v.page_gap();
// [0, 1]: 300 + 600 wide, 800 high, one gap between them.
let spread = v.first_page_now(&info).expect("a spread");
assert_eq!((spread.size, spread.margins.x), (vec2(900.0 * PT, 800.0 * PT), 2.0 * SIDE + gap));
// [2] on its own at the end: one page, no gap.
v.current = 2;
let last = v.first_page_now(&info).expect("the last page");
assert_eq!((last.size, last.margins.x), (vec2(200.0 * PT, 500.0 * PT), 2.0 * SIDE));
// With a cover page: [0] alone, then [1, 2].
v.set_cover(true);
assert_eq!(v.first_page_now(&info).expect("a spread").size, vec2(800.0 * PT, 800.0 * PT));
v.current = 0;
let cover = v.first_page_now(&info).expect("the cover");
assert_eq!((cover.size, cover.margins.x), (vec2(300.0 * PT, 400.0 * PT), 2.0 * SIDE));
v.current = 9;
assert!(v.first_page_now(&info).is_none());
}
#[test]
fn image_handles_account_for_document_and_view_rotation() {
for page_rotation in [0, 90, 180, 270] {
+25
View File
@@ -140,6 +140,31 @@ impl DocView {
self.layout == PageLayout::TwoUp
}
/// The page (or spread) the view shows first, as `FitWindow` sizes the window to it
/// ([`crate::viewer_prefs::window_commands`]). `None` when there is no such page.
pub(crate) fn first_page_now(&self, info: &DocInfo) -> Option<crate::viewer_prefs::PageNow> {
info.pages.get(self.current)?;
// Two-page view shows the spread the page is in (a cover page on its own); the spread
// is as wide as its pages side by side and as tall as the taller one, as `layout`
// places it. The last spread of an odd page count has one page.
let (first, last) = if self.layout == PageLayout::TwoUp {
let first = self.spread_first(self.current);
(first, if self.cover && first == 0 { 0 } else { first.saturating_add(1) })
} else {
(self.current, self.current)
};
let sizes: Vec<(f32, f32)> =
info.pages.iter().skip(first).take(last.saturating_sub(first).saturating_add(1)).map(|p| self.display_size(p)).collect();
let w: f32 = sizes.iter().map(|s| s.0).sum();
let h = sizes.iter().map(|s| s.1).fold(0.0, f32::max);
let gap = self.page_gap() * sizes.len().saturating_sub(1) as f32;
Some(crate::viewer_prefs::PageNow {
size: vec2(w * PT, h * PT),
margins: vec2(2.0 * SIDE + gap, 2.0 * MARGIN),
zoom: (self.fit == Fit::None).then_some(self.zoom),
})
}
/// Displayed page size in points for this view rotation.
pub(super) fn display_size(&self, p: &pdfcraft_render::PageInfo) -> (f32, f32) {
if self.rotation % 180 == 90 { (p.height, p.width) } else { (p.width, p.height) }
+7 -2
View File
@@ -588,19 +588,24 @@ const MODE_BAR_SEARCH_MIN: f32 = 140.0;
pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
// The document in the active tab may hide the menu or the tools (`crate::viewer_prefs`).
let hidden = app.hidden_interface();
egui::Panel::top("mode_bar")
.exact_size(48.0)
.frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin::symmetric(10, 0)).stroke(Stroke::new(1.0, t.divider)))
.show(ui, |ui| {
ui.horizontal_centered(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
// The menus, unless the native macOS menu bar has them.
if app.native_menu.is_none() {
// The menus, unless the native macOS menu bar has them or the document hides them.
if app.native_menu.is_none() && !hidden.menubar {
app.menu_bar_ui(ui);
ui.add_space(6.0);
ui.painter().vline(ui.cursor().left(), ui.max_rect().y_range().shrink(12.0), Stroke::new(1.0, t.divider));
ui.add_space(10.0);
}
if hidden.toolbar {
return;
}
// Acrobat's mode bar has no Read tab: reading is what the viewer does with no tool
// in use. Read mode (the panes hidden) stays a View command and a default workspace.
let modes = [(Mode::AllTools, "All tools"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")];
+6
View File
@@ -334,6 +334,7 @@ impl PdfCraftApp {
}
"view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false),
"view.split" => self.toggle_split(),
crate::viewer_prefs::TOGGLE => self.toggle_interface(),
"view.previous_view" | "view.next_view" => {
if let Some(v) = active.and_then(|i| self.views.get_mut(i)) {
return Some(v.view_history(id == "view.next_view"));
@@ -832,6 +833,11 @@ impl PdfCraftApp {
if matches!(spec.id, "view.previous_view" | "view.next_view") && !self.command_enabled(spec) {
continue;
}
// The OCR Verify screen keeps the window's toolbars and owns F8 and Shift+F8 (the next
// and previous word to review), so they don't show or hide the interface there.
if spec.id == crate::viewer_prefs::TOGGLE && self.ocr_verify.open {
continue;
}
// A box typed into on the page (a paragraph being edited, added text) keeps Undo and
// Redo only while it holds typing the page doesn't have, or Redo has typing to put
// back; then they reach the document. ⌘⇧Z is also ⌘Z to the Undo shortcut, so a
+1
View File
@@ -152,6 +152,7 @@ impl Host for crate::PdfCraftApp {
"tools_side": if self.side_panels.tools_on_right() { "right" } else { "left" },
"rail_side": if self.side_panels.rail_on_right() { "right" } else { "left" },
"right_panel": self.right.map(|r| format!("{r:?}")),
"hidden_interface": self.hidden_interface().to_json(),
"dialog": self.dialog.map(|d| format!("{d:?}")),
"palette_open": self.palette_open,
"theme": format!("{:?}", self.theme),
+4
View File
@@ -63,6 +63,7 @@ pub use print_ui::{Handling as PrintHandling, OptionsSource, PrintDraft, Printer
pub use redact_ui::{HiddenDraft, PagesDraft as RedactPagesDraft, RedactPrefs, SearchDraft as RedactSearchDraft, SearchMode as RedactSearchMode};
pub mod i18n;
pub mod theme;
pub mod viewer_prefs;
mod widgets;
use pdfcraft_engine::{DocId, Session};
@@ -791,6 +792,9 @@ pub struct PdfCraftApp {
/// A document just opened asks to be shown in Full Screen mode (`/PageMode /FullScreen`):
/// the person is asked first, as in Acrobat, and this is the document waiting for the answer.
pub pending_full_screen: Option<DocId>,
/// A document just opened asks for its window to fit its first page or to be centred
/// (`FitWindow`, `CenterWindow`): done once the page is laid out.
pub pending_window: Option<viewer_prefs::PendingWindow>,
/// An absolute GoToR target waiting for approval, before any target-file I/O (#1364).
pub pending_remote_link: Option<PendingRemoteLink>,
}
+1 -1
View File
@@ -173,7 +173,7 @@ const DISPLAY: &[&str] = &[
const THEMES: &[&str] = &["view.theme.system", "view.theme.light", "view.theme.dark"];
const PANES: &[&str] = &["view.side_panel", "view.tools_pane", "view.split"];
const PANE_COMMANDS: &[&str] = &["comment.list", "form.fields", "ui.dock.reset"];
const MODES: &[&str] = &["view.full_screen", "view.read_mode"];
const MODES: &[&str] = &["view.full_screen", "view.read_mode", crate::viewer_prefs::TOGGLE];
const PAGES: &[&str] = &[
"page.organize",
SEP,
+374
View File
@@ -0,0 +1,374 @@
//! Viewer preferences that change the window and its interface when a document opens
//! (ISO 32000-2 §12.2, Table 147): `HideToolbar`, `HideMenubar` and `HideWindowUI` hide parts of
//! the interface for that document's tab, and `FitWindow` and `CenterWindow` size and place the
//! window around its first page.
//!
//! The hidden interface is the tab's own and is never saved: another tab shows the usual
//! interface, and the command [`TOGGLE`] (F8) brings it back, so a document can't leave the user
//! without the toolbar, menu or panels.
use egui::{Pos2, Rect, Vec2, ViewportCommand, vec2};
/// The command that shows the interface a document hid, and hides it again.
pub const TOGGLE: &str = "view.toggle_interface";
/// Which parts of the interface are hidden in a document's tab.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HiddenInterface {
/// The mode bar's tools: the mode tabs, Save, Print, Properties and the command search, and
/// the floating quick-action bar (`HideToolbar`).
pub toolbar: bool,
/// The menu bar (`HideMenubar`).
pub menubar: bool,
/// The right rail with its page navigation, the side panels and the document's scroll bars,
/// "leaving only the document's contents displayed" (`HideWindowUI`).
pub window_ui: bool,
}
impl HiddenInterface {
pub const NONE: Self = Self { toolbar: false, menubar: false, window_ui: false };
pub const ALL: Self = Self { toolbar: true, menubar: true, window_ui: true };
/// What a document's viewer preferences ask to hide.
pub fn asked_by(v: &pdfcraft_engine::InitialView) -> Self {
Self { toolbar: v.hide_toolbar, menubar: v.hide_menubar, window_ui: v.hide_window_ui }
}
pub fn any(self) -> bool {
self.toolbar || self.menubar || self.window_ui
}
/// The mode bar has nothing left to show: the tools are hidden, and so are the menus, by the
/// document or because the native macOS menu bar (`native_menu`) has them instead.
pub fn mode_bar(self, native_menu: bool) -> bool {
self.toolbar && (self.menubar || native_menu)
}
/// For the control channel's `ui.state`.
pub fn to_json(self) -> serde_json::Value {
serde_json::json!({ "toolbar": self.toolbar, "menubar": self.menubar, "window_ui": self.window_ui })
}
}
/// A tab's interface: what its document asked to hide, and what is hidden now.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TabInterface {
pub asked: HiddenInterface,
pub hidden: HiddenInterface,
}
impl TabInterface {
/// The interface a document opens with: what it asks to hide, unless the user's preference
/// is to open every document their own way.
pub fn opening(asked: HiddenInterface, ignore_document: bool) -> Self {
if ignore_document { Self::default() } else { Self { asked, hidden: asked } }
}
/// [`TOGGLE`]: show everything that is hidden; with nothing hidden, hide what the document
/// asked to hide, or everything when it asked for nothing. Returns whether it is now hidden.
pub fn toggle(&mut self) -> bool {
self.hidden = if self.hidden.any() {
HiddenInterface::NONE
} else if self.asked.any() {
self.asked
} else {
HiddenInterface::ALL
};
self.hidden.any()
}
}
/// `FitWindow` and `CenterWindow`, waiting for the document's first frame (the size of its page
/// on screen is known only once it is laid out).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PendingWindow {
pub doc: pdfcraft_engine::DocId,
pub fit: bool,
pub center: bool,
/// Frames left to wait for the document to be laid out before giving up.
pub frames_left: u8,
}
/// What the window looks like now, as the windowing layer reports it (points).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowNow {
/// The window's content area.
pub inner: Vec2,
/// The window with its frame, where the windowing layer knows it.
pub outer: Option<Rect>,
pub monitor: Option<Vec2>,
/// The document area inside the window.
pub document_area: Vec2,
pub maximized: bool,
pub fullscreen: bool,
}
/// The first displayed page (or spread) as the view lays it out.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PageNow {
/// Its width and height on screen at 100%, in points.
pub size: Vec2,
/// The space the view keeps around it (gutters, margins, the gap in a spread), in points.
pub margins: Vec2,
/// The view's set zoom (1.0 = 100%); `None` when the view fits the page to the window.
pub zoom: Option<f32>,
}
/// Smallest window the fit makes, so a tiny page doesn't leave a sliver of a window.
const MIN_INNER: Vec2 = vec2(480.0, 360.0);
/// Share of the monitor the fitted window may take, leaving room for the desktop's own bars.
const MONITOR_SHARE: f32 = 0.9;
/// The viewport commands for `FitWindow` / `CenterWindow`. None for a maximized or full-screen
/// window (resizing it would fight the user's choice), or when the sizes are unusable.
///
/// `FitWindow` keeps the document area's height and makes the window as wide as the page needs
/// at that height when the view fits the page to the window, and makes the document area the
/// page's size on screen when the view has a set zoom; either way within the monitor.
/// `CenterWindow` centres the window on the monitor when the window is on the monitor at the
/// desktop's origin (the windowing layer reports the monitor's size but not where it is).
pub fn window_commands(now: WindowNow, page: PageNow, fit: bool, center: bool) -> Vec<ViewportCommand> {
let mut out = Vec::new();
if now.maximized || now.fullscreen || !usable(now.inner) || !usable(now.document_area) {
return out;
}
let chrome = (now.inner - now.document_area).max(Vec2::ZERO);
let mut inner = now.inner;
let zoom_ok = page.zoom.is_none_or(|z| z.is_finite() && z > 0.0);
if fit && zoom_ok && usable(page.size) && page.margins.x.is_finite() && page.margins.y.is_finite() {
let area = match page.zoom {
// Fitted to the window: keep the document area's height, and make the area as wide
// as the page is at the zoom that height gives it.
None => {
let zoom = ((now.document_area.y - page.margins.y) / page.size.y).max(0.0);
vec2(page.size.x * zoom + page.margins.x, now.document_area.y)
}
Some(zoom) => page.size * zoom + page.margins,
};
let mut target = area + chrome;
if let Some(monitor) = now.monitor.filter(|m| usable(*m)) {
target = target.min(monitor * MONITOR_SHARE);
}
target = target.max(MIN_INNER).round();
if usable(target) && (target - now.inner).length() >= 1.0 {
out.push(ViewportCommand::InnerSize(target));
inner = target;
}
}
if center
&& let (Some(outer), Some(monitor)) = (now.outer, now.monitor.filter(|m| usable(*m)))
&& Rect::from_min_size(Pos2::ZERO, monitor).contains(outer.min)
{
// The frame around the content area stays the size it is.
let frame = (outer.size() - now.inner).max(Vec2::ZERO);
let size = inner + frame;
let at = ((monitor - size) / 2.0).max(Vec2::ZERO).round();
if at.x.is_finite() && at.y.is_finite() && (at - outer.min.to_vec2()).length() >= 1.0 {
out.push(ViewportCommand::OuterPosition(at.to_pos2()));
}
}
out
}
fn usable(v: Vec2) -> bool {
v.x.is_finite() && v.y.is_finite() && v.x >= 1.0 && v.y >= 1.0
}
impl crate::PdfCraftApp {
/// The window and interface preferences of the document opening in tab `index`
/// ([`Self::apply_initial_view`] has applied its navigation, layout, zoom and page).
///
/// - `HideToolbar`, `HideMenubar`, `HideWindowUI` hide those parts of the interface in its tab,
/// unless Always use my default zoom and page layout is on, with a notice of the key that
/// brings them back.
/// - A document that shows one page or one spread at a time (`/PageLayout /SinglePage`,
/// `/TwoPageLeft`, `/TwoPageRight`) and names no zoom opens at fit page instead of the
/// default zoom. ISO 32000-2 doesn't say which zoom such a layout opens at.
/// - `FitWindow`, `CenterWindow` wait for the page to be laid out
/// ([`Self::window_preferences_frame`]), and only for the window's only document: a
/// document opened in a tab beside others doesn't resize or move their window.
pub(crate) fn apply_viewer_preferences(&mut self, index: usize, v: &pdfcraft_engine::InitialView, activate: bool) {
use pdfcraft_engine::{InitialLayout as L, Magnification as M};
let own_view = self.always_default_view;
let only_document = self.views.len() == 1 && !self.combine_tab.open;
let Some(view) = self.views.get_mut(index) else { return };
view.interface = TabInterface::opening(HiddenInterface::asked_by(v), own_view);
if !own_view && v.magnification == M::Default && matches!(v.layout, L::SinglePage | L::TwoUp | L::TwoUpCoverPage) {
view.fit = crate::canvas::Fit::Page;
}
let (doc, hidden) = (view.id, view.interface.hidden.any());
if activate && hidden {
self.notify_interface_hidden();
}
// A browser page can't resize or move its window.
if !cfg!(target_arch = "wasm32") && activate && only_document && !self.full_screen && (v.fit_window || v.center_window) {
self.pending_window = Some(PendingWindow { doc, fit: v.fit_window, center: v.center_window, frames_left: 30 });
}
}
/// The parts of the interface hidden in the active tab.
pub fn hidden_interface(&self) -> HiddenInterface {
self.active.and_then(|i| self.views.get(i)).map_or(HiddenInterface::NONE, |v| v.interface.hidden)
}
/// [`TOGGLE`] in the active tab.
pub(crate) fn toggle_interface(&mut self) {
let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) else { return };
if view.interface.toggle() {
self.notify_interface_hidden();
}
}
fn notify_interface_hidden(&mut self) {
let key = pdfcraft_engine::commands::command(TOGGLE).and_then(|c| c.shortcut).map(|s| s.label(cfg!(target_os = "macos"))).unwrap_or_default();
self.notify_fmt("This document hides the toolbars and panels. Press {key} to show them.", &[("key", &key)]);
}
/// `FitWindow` and `CenterWindow` for the document just opened, once its first page is laid
/// out. Dropped when another tab shows first, when another tab has opened behind it (it is no
/// longer the window's only document), or after a few frames without a layout.
pub(crate) fn window_preferences_frame(&mut self, ctx: &egui::Context) {
let Some(mut pending) = self.pending_window.take() else { return };
let Some((index, id)) = self.active_ids() else { return };
if id != pending.doc || self.views.len() != 1 || self.combine_tab.open {
return;
}
let Some(view) = self.views.get(index) else { return };
let area = view.viewport_rect();
let page = self.session.get(id).and_then(|d| view.first_page_now(&d.info));
let Some(page) = page.filter(|_| area.is_positive()) else {
if let Some(left) = pending.frames_left.checked_sub(1) {
pending.frames_left = left;
self.pending_window = Some(pending);
ctx.request_repaint();
}
return;
};
let content = ctx.content_rect().size();
let now = ctx.input(|i| {
let vp = i.viewport();
WindowNow {
inner: vp.inner_rect.map_or(content, |r| r.size()),
outer: vp.outer_rect,
monitor: vp.monitor_size,
document_area: area.size(),
maximized: vp.maximized.unwrap_or(false),
fullscreen: vp.fullscreen.unwrap_or(false),
}
});
for command in window_commands(now, page, pending.fit, pending.center) {
ctx.send_viewport_cmd(command);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn now() -> WindowNow {
WindowNow {
inner: vec2(1400.0, 900.0),
outer: Some(Rect::from_min_size(Pos2::new(50.0, 40.0), vec2(1400.0, 930.0))),
monitor: Some(vec2(1920.0, 1080.0)),
document_area: vec2(1300.0, 800.0),
maximized: false,
fullscreen: false,
}
}
const FITTED: PageNow = PageNow { size: vec2(200.0, 300.0), margins: vec2(140.0, 56.0), zoom: None };
#[test]
fn a_fitted_page_gets_a_window_of_its_shape() {
// An 800-point-high area fits the 300-point page at 248%: 496 points wide plus margins,
// and the 100 points of interface beside the document area.
let c = window_commands(now(), FITTED, true, false);
assert_eq!(c, vec![ViewportCommand::InnerSize(vec2(736.0, 900.0))]);
}
#[test]
fn a_set_zoom_gets_the_page_size_within_the_monitor() {
let page = PageNow { size: vec2(400.0, 500.0), margins: vec2(100.0, 100.0), zoom: Some(1.0) };
assert_eq!(window_commands(now(), page, true, false), vec![ViewportCommand::InnerSize(vec2(600.0, 700.0))]);
// Too tall for the monitor: the window stops at the share of the monitor it may take.
let page = PageNow { size: vec2(1000.0, 2000.0), margins: Vec2::ZERO, zoom: Some(1.0) };
assert_eq!(window_commands(now(), page, true, false), vec![ViewportCommand::InnerSize(vec2(1100.0, 972.0))]);
// A tiny page doesn't make a sliver of a window.
let page = PageNow { size: vec2(10.0, 10.0), margins: Vec2::ZERO, zoom: Some(1.0) };
assert_eq!(window_commands(now(), page, true, false), vec![ViewportCommand::InnerSize(MIN_INNER)]);
}
#[test]
fn a_maximized_or_full_screen_window_is_left_alone() {
assert!(window_commands(WindowNow { maximized: true, ..now() }, FITTED, true, true).is_empty());
assert!(window_commands(WindowNow { fullscreen: true, ..now() }, FITTED, true, true).is_empty());
// Nothing asked, nothing done.
assert!(window_commands(now(), FITTED, false, false).is_empty());
}
#[test]
fn centring_uses_the_new_size_and_the_frame() {
let c = window_commands(now(), FITTED, true, true);
// 736 wide and 900 + 30 of frame high, on 1920 × 1080.
assert_eq!(c.get(1), Some(&ViewportCommand::OuterPosition(Pos2::new(592.0, 75.0))));
// Centre alone keeps the size.
let c = window_commands(now(), FITTED, false, true);
assert_eq!(c, vec![ViewportCommand::OuterPosition(Pos2::new(260.0, 75.0))]);
// A window on another monitor (outside the one at the origin) isn't moved.
let away = WindowNow { outer: Some(Rect::from_min_size(Pos2::new(2000.0, 40.0), vec2(1400.0, 930.0))), ..now() };
assert!(window_commands(away, FITTED, false, true).is_empty());
// Nor is one whose monitor or frame is unknown.
assert!(window_commands(WindowNow { monitor: None, ..now() }, FITTED, false, true).is_empty());
assert!(window_commands(WindowNow { outer: None, ..now() }, FITTED, false, true).is_empty());
}
#[test]
fn unusable_sizes_do_nothing() {
for bad in [Vec2::ZERO, vec2(f32::NAN, 10.0), vec2(f32::INFINITY, 10.0), vec2(-5.0, 10.0)] {
let page = PageNow { size: bad, ..FITTED };
assert!(window_commands(now(), page, true, false).is_empty(), "{bad:?}");
assert!(window_commands(WindowNow { inner: bad, ..now() }, FITTED, true, true).is_empty(), "{bad:?}");
assert!(window_commands(WindowNow { document_area: bad, ..now() }, FITTED, true, true).is_empty(), "{bad:?}");
assert!(window_commands(WindowNow { monitor: Some(bad), ..now() }, FITTED, false, true).is_empty(), "{bad:?}");
}
for zoom in [f32::NAN, 0.0, -1.0, f32::INFINITY] {
assert!(window_commands(now(), PageNow { zoom: Some(zoom), ..FITTED }, true, false).is_empty(), "{zoom}");
}
let page = PageNow { margins: vec2(f32::NAN, 0.0), ..FITTED };
assert!(window_commands(now(), page, true, false).is_empty());
}
#[test]
fn toggling_restores_and_rehides() {
let asked = HiddenInterface { toolbar: true, ..HiddenInterface::NONE };
let mut tab = TabInterface::opening(asked, false);
assert_eq!(tab.hidden, asked);
assert!(!tab.toggle());
assert_eq!(tab.hidden, HiddenInterface::NONE);
assert!(tab.toggle());
assert_eq!(tab.hidden, asked);
// The user's preference to open documents their own way hides nothing.
let mut own = TabInterface::opening(asked, true);
assert_eq!(own.hidden, HiddenInterface::NONE);
// A tab whose document asked for nothing hides everything, and shows it again.
assert!(own.toggle());
assert_eq!(own.hidden, HiddenInterface::ALL);
assert!(!own.toggle());
}
#[test]
fn the_mode_bar_goes_when_nothing_is_left_on_it() {
let toolbar = HiddenInterface { toolbar: true, ..HiddenInterface::NONE };
// The in-window menus are still there.
assert!(!toolbar.mode_bar(false));
// #4378 review: on macOS the native menu bar has the menus, so hiding the tools leaves an
// empty bar, which goes too.
assert!(toolbar.mode_bar(true));
let both = HiddenInterface { menubar: true, ..toolbar };
assert!(both.mode_bar(false) && both.mode_bar(true));
// Menus hidden, tools shown: the bar has the tools.
let menubar = HiddenInterface { menubar: true, ..HiddenInterface::NONE };
assert!(!menubar.mode_bar(false) && !menubar.mode_bar(true));
}
}
+1
View File
@@ -92,4 +92,5 @@ mod ui;
mod updates;
mod view;
mod viewer;
mod viewer_prefs;
mod xfa;
+335
View File
@@ -0,0 +1,335 @@
//! Viewer preferences applied when a document opens (ISO 32000-2 §12.2, Table 147): hiding the
//! toolbars, menu and window controls in its tab, with F8 to bring them back; fitting and
//! centring the window; and the zoom a one-page-at-a-time layout opens at.
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use pdfcraft_ui_egui::PdfCraftApp;
use pdfcraft_ui_egui::canvas::Fit;
use pdfcraft_ui_egui::viewer_prefs::HiddenInterface;
/// Two 200 × 300 pages; `catalog` is added to the catalog dictionary.
fn fixture(catalog: &str) -> Vec<u8> {
let objs = [
format!("<< /Type /Catalog /Pages 2 0 R {catalog} >>"),
"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 /MediaBox [0 0 200 300] >>".into(),
"<< /Type /Page /Parent 2 0 R >>".into(),
"<< /Type /Page /Parent 2 0 R >>".into(),
];
let mut out = b"%PDF-1.7\n".to_vec();
let mut offs = Vec::new();
for (i, o) in objs.iter().enumerate() {
offs.push(out.len());
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
}
let x = out.len();
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for o in offs {
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
}
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n", objs.len() + 1).as_bytes());
out
}
const HIDE_ALL: &str = "/ViewerPreferences << /HideToolbar true /HideMenubar true /HideWindowUI true >>";
/// A plain document in the first tab and `files` after it, the last one active.
fn harness(setup: impl FnOnce(&mut PdfCraftApp) + 'static, files: &'static [&'static str]) -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
setup(&mut app);
for (i, catalog) in files.iter().enumerate() {
app.open_bytes(&format!("{i}.pdf"), None, fixture(catalog)).unwrap();
}
app
});
settle(&mut h);
h
}
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
for _ in 0..3000 {
h.run_steps(2);
if !h.state().render_pending() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
h.run_steps(2);
}
/// Which of the toolbar (the command search), the menu (the menu bar's File menu) and the window
/// controls (the rail's fit button) are on screen.
fn shown(h: &Harness<'static, PdfCraftApp>) -> (bool, bool, bool) {
(
h.query_by_label("Find tools and commands").is_some(),
h.query_by(|n| n.role() == egui::accesskit::Role::MenuItem && n.label().as_deref() == Some("File")).is_some(),
h.query_by_label("Toggle fit page / fit width").is_some(),
)
}
#[test]
fn a_document_that_hides_the_interface_opens_without_it_and_f8_brings_it_back() {
let mut h = harness(|_| {}, &["", HIDE_ALL]);
assert_eq!(shown(&h), (false, false, false), "the toolbar, menu and rail are hidden");
assert_eq!(h.state().hidden_interface(), HiddenInterface::ALL);
// The quick-action bar is a toolbar too.
assert!(h.query_by_label_contains("Hand (H)").is_none());
// The tab strip stays: the tabs and Home are still there.
h.get_by_label("Home");
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true), "F8 shows them");
h.get_by_label_contains("Hand (H)");
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (false, false, false), "and hides them again");
// Another tab, whose document asks for nothing, has the usual interface.
h.state_mut().active = Some(0);
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true));
h.state_mut().active = Some(1);
h.run_steps(3);
assert_eq!(shown(&h), (false, false, false));
// The command (palette, menus, automation) does what F8 does.
assert!(h.state_mut().execute("view.toggle_interface"));
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true));
}
/// Reloading a file changed on disk keeps the tab's interface: hidden stays hidden, and F8 still
/// hides only what the document asked for.
#[test]
fn a_reloaded_document_keeps_its_interface() {
let mut h = harness(|_| {}, &["/ViewerPreferences << /HideToolbar true >>"]);
let reload = |h: &mut Harness<'static, PdfCraftApp>| {
let info = h.state().session.get(h.state().views[0].id).unwrap().info.clone();
h.state_mut().views[0].document_replaced(&info);
h.run_steps(3);
};
reload(&mut h);
assert_eq!(shown(&h), (false, true, true), "still hidden after the reload");
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true));
reload(&mut h);
assert_eq!(shown(&h), (true, true, true), "still shown after the reload");
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (false, true, true), "F8 hides only what the document asked for");
}
#[test]
fn each_preference_hides_its_own_part() {
let top = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].viewport_rect().top();
let mut h = harness(|_| {}, &["/ViewerPreferences << /HideToolbar true >>"]);
assert_eq!(shown(&h), (false, true, true), "HideToolbar");
// Shown and hidden again, F8 hides only what the document asked for.
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true));
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (false, true, true));
let h = harness(|_| {}, &["/ViewerPreferences << /HideMenubar true >>"]);
assert_eq!(shown(&h), (true, false, true), "HideMenubar");
let h = harness(|_| {}, &["/ViewerPreferences << /HideWindowUI true >>"]);
assert_eq!(shown(&h), (true, true, false), "HideWindowUI");
assert!(h.state().hidden_interface().window_ui);
// False is the default, and is nothing to hide.
let h = harness(|_| {}, &["/ViewerPreferences << /HideToolbar false /HideMenubar false /HideWindowUI false >>"]);
assert_eq!(shown(&h), (true, true, true));
let with_bar = top(&h);
// With both the tools and the menu hidden, the mode bar goes and the document moves up.
let h = harness(|_| {}, &["/ViewerPreferences << /HideToolbar true /HideMenubar true >>"]);
assert_eq!(shown(&h), (false, false, true));
assert!(top(&h) < with_bar - 40.0, "{} against {with_bar}", top(&h));
}
#[test]
fn hiding_the_window_controls_leaves_the_document_the_whole_width() {
// The document also asks for the page thumbnails panel, which is hidden with the rest.
let mut h = harness(|_| {}, &["/PageMode /UseThumbs /ViewerPreferences << /HideWindowUI true >>"]);
let width = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].viewport_rect().width();
assert!(width(&h) > 1390.0, "{}", width(&h));
h.key_press(egui::Key::F8);
settle(&mut h);
// The rail and the Pages panel are back beside the document.
assert!(width(&h) < 1400.0 - 48.0 - 200.0, "{}", width(&h));
}
#[test]
fn the_preference_to_ignore_document_views_keeps_the_interface() {
let h = harness(|app| app.always_default_view = true, &[HIDE_ALL]);
assert_eq!(shown(&h), (true, true, true));
assert_eq!(h.state().hidden_interface(), HiddenInterface::NONE);
}
#[test]
fn f8_hides_the_interface_of_any_document_and_shows_it_again() {
let mut h = harness(|_| {}, &[""]);
assert_eq!(shown(&h), (true, true, true));
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (false, false, false));
h.get_by_label_contains("Press F8 to show them");
h.key_press(egui::Key::F8);
h.run_steps(3);
assert_eq!(shown(&h), (true, true, true));
}
#[test]
fn hiding_the_interface_is_never_saved() {
let h = harness(|_| {}, &[HIDE_ALL]);
let settings = h.state().persist();
assert!(!settings.contains("interface"), "{settings}");
}
/// The fit a document with `catalog` opens at, with the user's defaults (fit width).
fn opening_fit(catalog: &str, own_view: bool) -> Fit {
let mut app = PdfCraftApp::new();
app.always_default_view = own_view;
app.open_bytes("x.pdf", None, fixture(catalog)).unwrap();
app.views[0].fit
}
#[test]
fn one_page_at_a_time_opens_at_fit_page() {
let default = opening_fit("", false);
assert_eq!(default, Fit::Width, "the default zoom");
for layout in ["SinglePage", "TwoPageLeft", "TwoPageRight"] {
assert_eq!(opening_fit(&format!("/PageLayout /{layout}"), false), Fit::Page, "{layout}");
// The user's preference keeps their zoom.
assert_eq!(opening_fit(&format!("/PageLayout /{layout}"), true), default, "{layout}");
}
// Scrolling layouts keep the default zoom.
for layout in ["OneColumn", "TwoColumnLeft", "TwoColumnRight"] {
assert_eq!(opening_fit(&format!("/PageLayout /{layout}"), false), default, "{layout}");
}
// A zoom the document names wins.
assert_eq!(opening_fit("/PageLayout /SinglePage /OpenAction [3 0 R /FitH 300]", false), Fit::Width);
let mut app = PdfCraftApp::new();
app.open_bytes("x.pdf", None, fixture("/PageLayout /SinglePage /OpenAction [3 0 R /XYZ 0 300 2]")).unwrap();
assert_eq!((app.views[0].fit, app.views[0].zoom), (Fit::None, 2.0));
}
/// Review on #4378: where the reader left off wins over the fit page a one-page-at-a-time
/// layout opens at.
#[test]
fn a_remembered_zoom_wins_over_the_layouts_fit_page() {
let path = std::env::temp_dir().join(format!("pdfcraft-viewer-prefs-last-view-{}.pdf", std::process::id())).to_string_lossy().into_owned();
let mut app = PdfCraftApp::new();
let json_path = serde_json::to_string(&path).unwrap();
app.restore(&format!(r#"{{"last_views": [{{"path": {json_path}, "page": 1, "zoom": "62.5%", "display": "single"}}]}}"#));
assert!(app.restore_last_view);
app.open_bytes("x.pdf", Some(path), fixture("/PageLayout /SinglePage")).unwrap();
let v = &app.views[0];
assert_eq!((v.fit, v.zoom, v.current), (Fit::None, 0.625, 1));
}
/// The last `InnerSize` and `OuterPosition` asked for over `frames` frames.
fn window_commands(h: &mut Harness<'static, PdfCraftApp>, frames: usize) -> (Option<egui::Vec2>, Option<egui::Pos2>) {
let (mut size, mut at) = (None, None);
for _ in 0..frames {
h.run_steps(1);
for c in &h.output().viewport_output[&egui::ViewportId::ROOT].commands {
match c {
egui::ViewportCommand::InnerSize(s) => size = Some(*s),
egui::ViewportCommand::OuterPosition(p) => at = Some(*p),
_ => {}
}
}
}
(size, at)
}
fn on_monitor(h: &mut Harness<'static, PdfCraftApp>) {
let vp = h.input_mut().viewports.entry(egui::ViewportId::ROOT).or_default();
vp.monitor_size = Some(egui::vec2(1920.0, 1080.0));
vp.inner_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 70.0), egui::vec2(1400.0, 900.0)));
vp.outer_rect = Some(egui::Rect::from_min_size(egui::pos2(50.0, 40.0), egui::vec2(1400.0, 930.0)));
}
/// A harness whose window is on a 1920 × 1080 monitor, with one document opened in the first
/// frames.
fn windowed(files: &'static [&'static str]) -> Harness<'static, PdfCraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app
});
on_monitor(&mut h);
h.run_steps(2);
for (i, catalog) in files.iter().enumerate() {
h.state_mut().open_bytes(&format!("{i}.pdf"), None, fixture(catalog)).unwrap();
}
h
}
#[test]
fn fit_window_sizes_the_window_to_the_first_page() {
let mut h = windowed(&["/ViewerPreferences << /FitWindow true >> /OpenAction [3 0 R /Fit]"]);
let (size, at) = window_commands(&mut h, 12);
let size = size.expect("the window is resized");
assert_eq!(at, None, "not moved");
// A 2:3 portrait page fitted to the window's height makes a narrower window.
assert!(size.x < 1000.0 && (size.y - 900.0).abs() < 1.0, "{size:?}");
// Asked once.
assert_eq!(window_commands(&mut h, 6), (None, None));
// The page now fills the document area's height and nearly its width.
let v = &h.state().views[0];
let (page, area) = (v.page_screen_rect(0).expect("shown"), v.viewport_rect());
assert!(area.width() - page.width() < 160.0, "page {page:?} in {area:?}");
}
#[test]
fn center_window_centres_the_window_on_the_monitor() {
let mut h = windowed(&["/ViewerPreferences << /CenterWindow true >>"]);
// 1400 × 930 on 1920 × 1080.
assert_eq!(window_commands(&mut h, 12), (None, Some(egui::pos2(260.0, 75.0))));
let mut h = windowed(&["/ViewerPreferences << /FitWindow true /CenterWindow true >>"]);
let (size, at) = window_commands(&mut h, 12);
let (size, at) = (size.expect("resized"), at.expect("moved"));
assert!(((1920.0 - size.x) / 2.0 - at.x).abs() <= 0.5, "{size:?} at {at:?}");
}
#[test]
fn a_maximized_window_or_a_second_tab_is_left_alone() {
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| PdfCraftApp::new());
on_monitor(&mut h);
h.input_mut().viewports.entry(egui::ViewportId::ROOT).or_default().maximized = Some(true);
h.run_steps(2);
h.state_mut().open_bytes("0.pdf", None, fixture("/ViewerPreferences << /FitWindow true /CenterWindow true >>")).unwrap();
assert_eq!(window_commands(&mut h, 12), (None, None), "maximized");
let mut h = windowed(&["", "/ViewerPreferences << /FitWindow true /CenterWindow true >>"]);
assert_eq!(window_commands(&mut h, 12), (None, None), "beside another document");
// A tab opened behind the document before its page is laid out (a late startup file) leaves
// the window alone too, though the document stays active.
let mut h = windowed(&["/ViewerPreferences << /FitWindow true /CenterWindow true >>"]);
h.state_mut().open_bytes("1.pdf", None, fixture("")).unwrap();
h.state_mut().active = Some(0);
assert_eq!(window_commands(&mut h, 12), (None, None), "a tab opened behind it");
}
#[test]
fn the_user_is_told_which_key_brings_the_interface_back() {
let mut h = windowed(&[HIDE_ALL]);
h.run_steps(3);
h.get_by_label_contains("This document hides the toolbars and panels. Press F8 to show them.");
// Not when nothing is hidden.
let mut h = windowed(&["/ViewerPreferences << /HideToolbar false /HideMenubar false /HideWindowUI false >>"]);
h.run_steps(3);
assert!(h.query_by_label_contains("Press F8").is_none());
// Nor when the user opens documents their own way.
let mut h = windowed(&[]);
h.state_mut().always_default_view = true;
h.state_mut().open_bytes("0.pdf", None, fixture(HIDE_ALL)).unwrap();
h.run_steps(3);
assert!(h.query_by_label_contains("Press F8").is_none());
}
+3 -3
View File
@@ -3584,10 +3584,10 @@ area = "D"
tier = "P1"
milestone = "M4"
status = "shipped"
commands = ["file.properties"]
commands = ["file.properties", "view.toggle_interface"]
tools = ["doc_initial_view"]
evidence = ["crates/organize/src/view.rs::initial_view_resolves_indirect_boolean_preferences", "crates/ui-egui/tests/it/ui.rs::a_document_asking_for_full_screen_is_asked_about_first", "crates/organize/src/tests.rs::initial_view_round_trips_and_keeps_scripts", "crates/ui-egui/tests/it/viewer.rs::initial_view_is_edited_and_honoured_on_open", "crates/ui-egui/tests/it/viewer.rs::initial_view_magnification_offers_25_and_100_percent", "crates/organize/src/tests.rs::choosing_100_percent_keeps_an_actual_size_opening_destination", "crates/automation/tests/it/automation.rs::initial_view_through_tools", "crates/render/src/inspect/navigation.rs::a_go_to_action_opens_at_its_destination_explicit_or_named", "crates/ui-egui/tests/it/open_action.rs::an_xyz_open_action_puts_its_point_at_the_window_top_left", "crates/ui-egui/tests/it/open_action.rs::a_fith_open_action_puts_its_top_at_the_window_top", "crates/ui-egui/tests/it/open_action.rs::a_named_open_destination_opens_its_page", "crates/ui-egui/tests/it/open_action.rs::a_full_screen_document_shows_its_non_full_screen_panel_outside_full_screen", "crates/organize/src/tests.rs::an_unchanged_opening_destination_keeps_its_position_and_name", "crates/automation/tests/it/automation.rs::doc_info_reports_the_open_destination", "crates/organize/src/tests.rs::choosing_page_one_replaces_a_named_opening_destination", "crates/organize/src/tests.rs::a_named_opening_destination_is_kept_while_it_still_holds"]
notes = "navigation panel, layout, magnification, page, window and interface options; honoured on open; script open actions kept. Open in Full Screen mode (/PageMode /FullScreen) asks first (Full Screen / Not now), as Acrobat does, instead of taking the screen. Window and interface options also read indirect boolean ViewerPreferences values. Missing: Acrobat's Full Screen preference to turn that alert off and its \"remember my choice for this document\" (misc.prefs-full-screen). On open the /OpenAction destination places the view as ISO 32000-2 Table 149 says (the /XYZ point at the window's top-left, /FitH and /FitV top or left at the edge), and a named destination (name or string, /Names /Dests tree or catalog /Dests, §12.3.2.4) resolves; Always use my default zoom keeps only the page. A full-screen document shows its /NonFullScreenPageMode panel outside full screen (Table 147). doc_info reports the opening destination. Document Properties keeps an opening action whose page and magnification are left alone, but still edits only those two; it shows a named destination's page and magnification, and choosing another page or magnification replaces the name with an explicit destination, except that page 1 at the default magnification removes /OpenAction so the first page opens. A name that resolves nowhere reads as page 1 and is kept until another page is chosen"
evidence = ["crates/organize/src/view.rs::initial_view_resolves_indirect_boolean_preferences", "crates/ui-egui/tests/it/ui.rs::a_document_asking_for_full_screen_is_asked_about_first", "crates/organize/src/tests.rs::initial_view_round_trips_and_keeps_scripts", "crates/ui-egui/tests/it/viewer.rs::initial_view_is_edited_and_honoured_on_open", "crates/ui-egui/tests/it/viewer.rs::initial_view_magnification_offers_25_and_100_percent", "crates/organize/src/tests.rs::choosing_100_percent_keeps_an_actual_size_opening_destination", "crates/automation/tests/it/automation.rs::initial_view_through_tools", "crates/render/src/inspect/navigation.rs::a_go_to_action_opens_at_its_destination_explicit_or_named", "crates/ui-egui/tests/it/open_action.rs::an_xyz_open_action_puts_its_point_at_the_window_top_left", "crates/ui-egui/tests/it/open_action.rs::a_fith_open_action_puts_its_top_at_the_window_top", "crates/ui-egui/tests/it/open_action.rs::a_named_open_destination_opens_its_page", "crates/ui-egui/tests/it/open_action.rs::a_full_screen_document_shows_its_non_full_screen_panel_outside_full_screen", "crates/organize/src/tests.rs::an_unchanged_opening_destination_keeps_its_position_and_name", "crates/automation/tests/it/automation.rs::doc_info_reports_the_open_destination", "crates/organize/src/tests.rs::choosing_page_one_replaces_a_named_opening_destination", "crates/organize/src/tests.rs::a_named_opening_destination_is_kept_while_it_still_holds", "crates/ui-egui/tests/it/viewer_prefs.rs::a_document_that_hides_the_interface_opens_without_it_and_f8_brings_it_back", "crates/ui-egui/tests/it/viewer_prefs.rs::one_page_at_a_time_opens_at_fit_page", "crates/ui-egui/tests/it/viewer_prefs.rs::fit_window_sizes_the_window_to_the_first_page"]
notes = "navigation panel, layout, magnification, page, window and interface options; honoured on open; script open actions kept. Open in Full Screen mode (/PageMode /FullScreen) asks first (Full Screen / Not now), as Acrobat does, instead of taking the screen. Window and interface options also read indirect boolean ViewerPreferences values. Missing: Acrobat's Full Screen preference to turn that alert off and its \"remember my choice for this document\" (misc.prefs-full-screen). On open the /OpenAction destination places the view as ISO 32000-2 Table 149 says (the /XYZ point at the window's top-left, /FitH and /FitV top or left at the edge), and a named destination (name or string, /Names /Dests tree or catalog /Dests, §12.3.2.4) resolves; Always use my default zoom keeps only the page. A full-screen document shows its /NonFullScreenPageMode panel outside full screen (Table 147). doc_info reports the opening destination. Document Properties keeps an opening action whose page and magnification are left alone, but still edits only those two; it shows a named destination's page and magnification, and choosing another page or magnification replaces the name with an explicit destination, except that page 1 at the default magnification removes /OpenAction so the first page opens. A name that resolves nowhere reads as page 1 and is kept until another page is chosen. HideToolbar, HideMenubar and HideWindowUI are applied in the document's tab only (the mode bar's tools and quick-action bar, the menu bar, the rail, side panels and scroll bars; the tab strip stays), never saved, skipped with Always use my default zoom and page layout, and F8 (view.toggle_interface) brings them back. FitWindow and CenterWindow apply to the window's only document when the window is neither maximized nor full screen; centring needs the window on the monitor at the desktop origin, and Wayland and the browser ignore both. SinglePage, TwoPageLeft and TwoPageRight without a named zoom open at fit page"
[[feature]]
id = "organize.custom-properties"