use pdfrum_form::Event;
use pdfrum_form::FormSession as Inner;
pub use pdfrum_form::{Button, Key, Modifiers, Response};
pub use pdfrum_form::SessionConfig;
use crate::Document;
use pdfrum_common::PageIndex;
use pdfrum_page::BuildContext;
pub use pdfrum_form::{AppearanceUpdate, UpdateKind};
pub use pdfrum_form::{Cascade, FieldRef, FieldWrites, Keystroke, KeystrokeOutcome, NoScripts};
#[derive(Debug)]
pub struct FormSession<'a> {
pub(crate) doc: &'a Document,
pub(crate) state: State,
}
#[derive(Debug)]
pub(crate) struct State {
pub(crate) inner: Inner,
pages: std::collections::BTreeMap<PageIndex, pdfrum_form::PageForm>,
pub(crate) viewed_page: PageIndex,
pub(crate) fonts: std::sync::Arc<pdfrum_doc::ap::FormFonts>,
cascade: Cascades,
}
impl State {
pub(crate) fn vacant(fonts: &std::sync::Arc<pdfrum_doc::ap::FormFonts>) -> State {
State {
inner: Inner::with_config(SessionConfig {
focusable: Vec::new(),
..SessionConfig::default()
}),
pages: std::collections::BTreeMap::new(),
viewed_page: PageIndex::FIRST,
fonts: std::sync::Arc::clone(fonts),
cascade: Cascades::Plain(Box::new(NoScripts)),
}
}
#[cfg(feature = "javascript")]
pub(crate) fn script_failures(
&mut self,
diags: &mut pdfrum_common::Diagnostics,
) -> Vec<crate::ScriptFailure> {
match &mut self.cascade {
Cascades::Scripted(cascade) => cascade.drain_diagnostics(diags),
Cascades::Plain(_) => Vec::new(),
}
}
pub(crate) fn focused_annot(&self) -> Option<pdfrum_form::AnnotId> {
self.inner.focus.map(pdfrum_form::FocusTarget::annot)
}
pub(crate) fn hover_for_page(&self, page: impl Into<PageIndex>) -> Option<usize> {
let page = page.into();
let hover = self.inner.hover?;
(hover.page == page).then_some(hover.index as usize)
}
pub(crate) fn can_undo(&self) -> bool {
match self.inner.focused_state() {
Some(pdfrum_form::field::FieldState::Text(text)) => text.edit.undo.can_undo(),
Some(
pdfrum_form::field::FieldState::Choice(_)
| pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_),
)
| None => false,
}
}
pub(crate) fn can_redo(&self) -> bool {
match self.inner.focused_state() {
Some(pdfrum_form::field::FieldState::Text(text)) => text.edit.undo.can_redo(),
Some(
pdfrum_form::field::FieldState::Choice(_)
| pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_),
)
| None => false,
}
}
pub(crate) fn focused_text(&self) -> Option<String> {
match self.inner.focused_state()? {
pdfrum_form::field::FieldState::Text(text) => Some(text.text().to_string()),
pdfrum_form::field::FieldState::Choice(choice) => Some(choice.focused_text()),
pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_) => None,
}
}
pub(crate) fn selected_text(&self) -> Option<String> {
match self.inner.focused_state()? {
pdfrum_form::field::FieldState::Text(text) => Some(text.edit.selected_text()),
pdfrum_form::field::FieldState::Choice(choice) => Some(
choice
.edit
.as_ref()
.map(|edit| edit.selected_text())
.unwrap_or_default(),
),
pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_) => None,
}
}
pub(crate) fn is_index_selected(&self, index: usize) -> bool {
match self.inner.focused_state() {
Some(pdfrum_form::field::FieldState::Choice(choice)) => {
pdfrum_form::field::choice::is_index_selected(choice, index)
}
Some(
pdfrum_form::field::FieldState::Text(_)
| pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_),
)
| None => false,
}
}
pub(crate) fn set_index_selected(&mut self, index: usize, selected: bool) -> bool {
match self.inner.focused_state_mut() {
Some(pdfrum_form::field::FieldState::Choice(choice)) => {
pdfrum_form::field::choice::set_index_selected(choice, index, selected)
}
Some(
pdfrum_form::field::FieldState::Text(_)
| pdfrum_form::field::FieldState::Toggle(_)
| pdfrum_form::field::FieldState::Button(_),
)
| None => false,
}
}
#[cfg(feature = "javascript")]
pub(crate) fn scripts(&self) -> Option<&pdfrum_form::ScriptCascade> {
match &self.cascade {
Cascades::Scripted(cascade) => Some(cascade),
Cascades::Plain(_) => None,
}
}
#[cfg(feature = "javascript")]
pub(crate) fn scripts_mut(&mut self) -> Option<&mut pdfrum_form::ScriptCascade> {
match &mut self.cascade {
Cascades::Scripted(cascade) => Some(cascade),
Cascades::Plain(_) => None,
}
}
#[cfg(feature = "javascript")]
pub(crate) fn advance_time(&mut self, elapsed: std::time::Duration) -> usize {
self.scripts_mut()
.map_or(0, |cascade| cascade.advance_time(elapsed))
}
}
enum Cascades {
Plain(Box<dyn Cascade>),
#[cfg(feature = "javascript")]
Scripted(Box<pdfrum_form::ScriptCascade>),
}
impl Cascades {
fn as_dyn(&mut self) -> &mut dyn Cascade {
match self {
Cascades::Plain(cascade) => cascade.as_mut(),
#[cfg(feature = "javascript")]
Cascades::Scripted(cascade) => cascade.as_mut(),
}
}
}
impl std::fmt::Debug for Cascades {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Cascades::Plain(_) => f.write_str("Plain(..)"),
#[cfg(feature = "javascript")]
Cascades::Scripted(cascade) => f.debug_tuple("Scripted").field(cascade).finish(),
}
}
}
impl<'a> FormSession<'a> {
#[must_use]
pub fn new(doc: &'a Document) -> FormSession<'a> {
FormSession::build(doc, Inner::new(), &mut BuildContext::new())
}
#[must_use]
pub fn with_config(doc: &'a Document, config: SessionConfig) -> FormSession<'a> {
FormSession::build(doc, Inner::with_config(config), &mut BuildContext::new())
}
#[must_use]
pub fn with_context(doc: &'a Document, ctx: &mut BuildContext) -> FormSession<'a> {
FormSession::build(doc, Inner::new(), ctx)
}
#[must_use]
pub fn with_config_in(
doc: &'a Document,
config: SessionConfig,
ctx: &mut BuildContext,
) -> FormSession<'a> {
FormSession::build(doc, Inner::with_config(config), ctx)
}
#[must_use]
pub fn with_cascade(doc: &'a Document, cascade: impl Cascade + 'static) -> FormSession<'a> {
FormSession::build_with(
doc,
Inner::new(),
&mut BuildContext::new(),
Cascades::Plain(Box::new(cascade)),
)
}
#[cfg(feature = "javascript")]
pub fn with_scripts(
doc: &'a Document,
config: &pdfrum_form::ScriptConfig,
) -> Result<FormSession<'a>, crate::ScriptBuildError> {
let cascade = pdfrum_form::ScriptCascade::new(config)?;
let mut session = FormSession::build_with(
doc,
Inner::new(),
&mut BuildContext::new(),
Cascades::Scripted(Box::new(cascade)),
);
session.install_document_model();
session.install_calculation_order();
Ok(session)
}
#[cfg(feature = "javascript")]
#[cfg(feature = "javascript")]
pub fn open_document(&mut self) {
if !matches!(self.state.cascade, Cascades::Scripted(_)) {
return;
}
let requests = {
let catalog = self.doc.catalog();
let parser = self.doc.parser();
let mut found = Vec::new();
open_requests::name_tree_scripts(&catalog, parser, &mut found);
open_requests::open_action(&catalog, parser, &mut found);
found
};
if let Cascades::Scripted(cascade) = &mut self.state.cascade {
for request in requests {
match request {
open_requests::Request::Script { whence, source } => {
cascade.run(&source, &whence);
}
open_requests::Request::Named(name) => cascade.record_named_action(name),
}
}
}
self.honour_focus_requests();
}
#[cfg(feature = "javascript")]
pub fn script_failures(
&mut self,
diags: &mut pdfrum_common::Diagnostics,
) -> Vec<crate::ScriptFailure> {
self.state.script_failures(diags)
}
#[cfg(feature = "javascript")]
fn install_document_model(&mut self) {
let catalog = self.doc.catalog();
let info = self
.doc
.parser()
.trailer()
.dict(pdfrum_object::names::INFO, self.doc.parser());
let pages: Vec<pdfrum_object::Dict> = (0..self.doc.page_count())
.filter_map(|index| self.doc.page(index).ok())
.map(|page| page.dict.dict.clone())
.collect();
let model = pdfrum_form::script::model::read(
&catalog,
info.as_ref(),
&pages,
"",
self.doc.parser(),
);
if let Cascades::Scripted(cascade) = &mut self.state.cascade {
cascade.set_document(model);
}
}
#[cfg(feature = "javascript")]
fn install_calculation_order(&mut self) {
let Cascades::Scripted(cascade) = &mut self.state.cascade else {
return;
};
let catalog = self.doc.catalog();
let mut diags = pdfrum_common::Diagnostics::default();
let Some(form) =
pdfrum_doc::form::Form::load(&catalog, self.doc.parser(), &self.doc.limits, &mut diags)
else {
return;
};
let order = form
.calculation_order(&catalog, self.doc.parser())
.into_iter()
.map(|index| u32::try_from(index).unwrap_or(u32::MAX))
.collect();
cascade.set_calculation_order(order);
}
fn build(doc: &'a Document, inner: Inner, ctx: &mut BuildContext) -> FormSession<'a> {
FormSession::build_with(doc, inner, ctx, Cascades::Plain(Box::new(NoScripts)))
}
fn build_with(
doc: &'a Document,
inner: Inner,
ctx: &mut BuildContext,
cascade: Cascades,
) -> FormSession<'a> {
let fonts = pdfrum_doc::ap::FormFonts::load(&doc.catalog(), doc.parser(), ctx);
FormSession {
doc,
state: State {
inner,
pages: std::collections::BTreeMap::new(),
viewed_page: PageIndex::FIRST,
fonts,
cascade,
},
}
}
pub fn set_viewed_page(&mut self, page: impl Into<PageIndex>) {
self.state.viewed_page = page.into();
}
#[must_use]
pub fn viewed_page(&self) -> PageIndex {
self.state.viewed_page
}
#[must_use]
pub fn config(&self) -> &SessionConfig {
&self.state.inner.config
}
pub fn apply(&mut self, event: Event) -> Response {
match event {
Event::KeyDown { .. } | Event::Char { .. } => self.dispatch_keyboard(event),
_ => self.dispatch(self.state.viewed_page, event),
}
}
pub fn mouse_move(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
modifiers: Modifiers,
) -> Response {
self.dispatch(page, Event::MouseMove { at, modifiers })
}
pub fn mouse_down(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
modifiers: Modifiers,
) -> Response {
self.dispatch(
page,
Event::MouseDown {
button: Button::Left,
at,
modifiers,
},
)
}
pub fn mouse_up(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
modifiers: Modifiers,
) -> Response {
self.dispatch(
page,
Event::MouseUp {
button: Button::Left,
at,
modifiers,
},
)
}
pub fn double_click(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
modifiers: Modifiers,
) -> Response {
self.dispatch(page, Event::DoubleClick { at, modifiers })
}
pub fn mouse_wheel(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
delta: (i32, i32),
modifiers: Modifiers,
) -> Response {
self.dispatch(
page,
Event::MouseWheel {
at,
delta,
modifiers,
},
)
}
pub fn focus_at(
&mut self,
page: impl Into<PageIndex>,
at: kurbo::Point,
modifiers: Modifiers,
) -> Response {
self.dispatch(page, Event::Focus { at, modifiers })
}
pub fn key_down(&mut self, key: Key, modifiers: Modifiers) -> Response {
self.dispatch_keyboard(Event::KeyDown { key, modifiers })
}
pub fn character(&mut self, ch: char, modifiers: Modifiers) -> Response {
self.dispatch_keyboard(Event::Char { ch, modifiers })
}
pub fn blur(&mut self) -> Response {
let Some(page) = self.focused_annot().map(|annot| annot.page) else {
return Response::ignored();
};
self.with_page_scripted(page, pdfrum_form::kill_focus)
}
#[must_use]
pub fn focused_annot(&self) -> Option<pdfrum_form::AnnotId> {
self.state.focused_annot()
}
#[must_use]
pub fn focus_for_page(&mut self, page: impl Into<PageIndex>) -> Option<pdfrum_doc::ap::Focus> {
self.with_page(page, |inner, ctx| pdfrum_form::focus_of(inner, ctx))
}
#[must_use]
pub fn popup_for_page(&mut self, page: impl Into<PageIndex>) -> Option<pdfrum_form::PopupView> {
self.with_page(page, |inner, ctx| pdfrum_form::popup_view(inner, ctx))
}
#[must_use]
pub fn scroll_view(&mut self, annot: pdfrum_form::AnnotId) -> Option<pdfrum_form::ScrollView> {
self.with_page(annot.page, |inner, ctx| {
pdfrum_form::scroll_view(inner, ctx, annot)
})
}
pub fn choose(&mut self, annot: pdfrum_form::AnnotId, index: usize) -> Response {
self.with_page_scripted(annot.page, |inner, ctx, cascade| {
pdfrum_form::route::choose(inner, ctx, cascade, annot, index)
})
}
pub fn close_popup(&mut self, annot: pdfrum_form::AnnotId) -> Response {
self.with_page(annot.page, |inner, ctx| {
pdfrum_form::route::close_popup(inner, ctx, annot)
})
}
#[must_use]
pub fn hover_for_page(&self, page: impl Into<PageIndex>) -> Option<usize> {
self.state.hover_for_page(page)
}
#[must_use]
pub fn can_undo(&self) -> bool {
self.state.can_undo()
}
#[must_use]
pub fn can_redo(&self) -> bool {
self.state.can_redo()
}
#[must_use]
pub fn focused_text(&self) -> Option<String> {
self.state.focused_text()
}
#[must_use]
pub fn selected_text(&self) -> Option<String> {
self.state.selected_text()
}
pub fn replace_selection(&mut self, text: &str) -> bool {
let Some(target) = self.state.inner.focus else {
return false;
};
let Some(field) = target.field() else {
return false;
};
let page = target.annot().page;
if !self.state.pages.contains_key(&page) {
let Some(read) = self.read_page(page) else {
return false;
};
self.state.pages.insert(page, read);
}
let Some(form) = self.state.pages.get(&page) else {
return false;
};
let catalog = self.doc.catalog();
let ctx = pdfrum_form::Context {
page: form,
catalog: &catalog,
resolve: self.doc.parser(),
fonts: &self.state.fonts,
permissions: self.permissions(),
};
pdfrum_form::route::replace_selection(&mut self.state.inner, &ctx, field, text)
}
#[must_use]
pub fn is_index_selected(&self, index: usize) -> bool {
self.state.is_index_selected(index)
}
pub fn set_index_selected(&mut self, index: usize, selected: bool) -> bool {
self.state.set_index_selected(index, selected)
}
#[cfg(feature = "javascript")]
#[must_use]
pub fn scripts(&self) -> Option<&pdfrum_form::ScriptCascade> {
self.state.scripts()
}
#[cfg(feature = "javascript")]
pub fn scripts_mut(&mut self) -> Option<&mut pdfrum_form::ScriptCascade> {
self.state.scripts_mut()
}
#[cfg(feature = "javascript")]
pub fn advance_time(&mut self, elapsed: std::time::Duration) -> usize {
self.state.advance_time(elapsed)
}
#[cfg(feature = "javascript")]
fn page_action(&self, page: PageIndex, opening: bool) -> Option<String> {
use pdfrum_doc::nav::AActionType;
let loaded = self.doc.page(page).ok()?;
let resolve = self.doc.parser();
let entries = loaded.dict.dict.dict(pdfrum_object::names::AA, resolve)?;
let trigger = if opening {
AActionType::OpenPage
} else {
AActionType::ClosePage
};
pdfrum_doc::nav::additional_action(&entries, trigger, resolve)
.filter(|action| action.kind() == pdfrum_doc::ActionKind::JavaScript)
.and_then(|action| action.javascript(resolve))
.filter(|source| !source.is_empty())
}
#[cfg(feature = "javascript")]
pub fn honour_focus_requests(&mut self) -> usize {
let mut moved = 0;
while let Some(index) = self
.scripts_mut()
.and_then(pdfrum_form::Cascade::take_focus_request)
{
let Some(page) = self.page_of_field(index) else {
continue;
};
self.with_page_scripted(page, |inner, ctx, cascade| {
pdfrum_form::focus_field(inner, ctx, cascade, index);
});
moved += 1;
}
moved
}
#[cfg(feature = "javascript")]
fn page_of_field(&mut self, index: u32) -> Option<PageIndex> {
let found = self
.state
.pages
.iter()
.find(|(_, form)| form.field_of_index(index).is_some())
.map(|(page, _)| *page);
if found.is_some() {
return found;
}
for page in 0..self.doc.page_count() {
let page = PageIndex::from(page);
self.ensure_page(page);
if self
.state
.pages
.get(&page)
.is_some_and(|form| form.field_of_index(index).is_some())
{
return Some(page);
}
}
None
}
#[cfg(feature = "javascript")]
pub fn page_opened(&mut self, page: impl Into<PageIndex>) {
let page = page.into();
let Some(source) = self.page_action(page, true) else {
return;
};
if let Some(cascade) = self.scripts_mut() {
cascade.run(&source, "/AA /O");
}
}
#[cfg(feature = "javascript")]
pub fn page_closed(&mut self, page: impl Into<PageIndex>) {
let page = page.into();
let Some(source) = self.page_action(page, false) else {
return;
};
if let Some(cascade) = self.scripts_mut() {
cascade.run(&source, "/AA /C");
}
}
pub fn load_page(&mut self, page: impl Into<PageIndex>) {
self.ensure_page(page.into());
}
fn dispatch(&mut self, page: impl Into<PageIndex>, event: Event) -> Response {
self.with_page_scripted(page, |inner, ctx, cascade| {
pdfrum_form::apply(inner, ctx, cascade, event)
})
}
fn with_page<T: Default>(
&mut self,
page: impl Into<PageIndex>,
body: impl FnOnce(
&mut pdfrum_form::FormSession,
&pdfrum_form::Context<'_, pdfrum_parser::Document>,
) -> T,
) -> T {
let page = page.into();
self.ensure_page(page);
let Some(form) = self.state.pages.get(&page) else {
return T::default();
};
let catalog = self.doc.catalog();
let ctx = pdfrum_form::Context {
page: form,
catalog: &catalog,
resolve: self.doc.parser(),
fonts: &self.state.fonts,
permissions: self.permissions(),
};
body(&mut self.state.inner, &ctx)
}
fn with_page_scripted<T: Default>(
&mut self,
page: impl Into<PageIndex>,
body: impl FnOnce(
&mut pdfrum_form::FormSession,
&pdfrum_form::Context<'_, pdfrum_parser::Document>,
&mut dyn Cascade,
) -> T,
) -> T {
let page = page.into();
self.ensure_page(page);
let mut cascade = std::mem::replace(
&mut self.state.cascade,
Cascades::Plain(Box::new(NoScripts)),
);
let out = self.with_page(page, |inner, ctx| body(inner, ctx, cascade.as_dyn()));
self.state.cascade = cascade;
out
}
fn dispatch_keyboard(&mut self, event: Event) -> Response {
if let Some(target) = self.state.inner.focus {
let page = pdfrum_form::FocusTarget::annot(target).page;
return self.dispatch(page, event);
}
match event {
Event::KeyDown { key: Key::Tab, .. } => self.dispatch(self.state.viewed_page, event),
_ => Response::ignored(),
}
}
fn ensure_page(&mut self, page: PageIndex) {
if self.state.pages.contains_key(&page) {
return;
}
let Some(read) = self.read_page(page) else {
return;
};
self.state.pages.insert(page, read);
self.install_page_scripts(page);
}
#[cfg(feature = "javascript")]
fn install_page_scripts(&mut self, page: PageIndex) {
use pdfrum_doc::nav::AActionType;
let Cascades::Scripted(cascade) = &mut self.state.cascade else {
return;
};
let Some(form) = self.state.pages.get(&page) else {
return;
};
let resolve = self.doc.parser();
let mut loaded: Vec<pdfrum_form::FieldRef> = Vec::new();
for widget in &form.widgets {
let Some(entries) = widget.dict.dict(pdfrum_object::names::AA, resolve) else {
continue;
};
let source_of = |trigger| {
pdfrum_doc::nav::additional_action(&entries, trigger, resolve)
.filter(|action| action.kind() == pdfrum_doc::ActionKind::JavaScript)
.and_then(|action| action.javascript(resolve))
.filter(|source| !source.is_empty())
};
let actions = pdfrum_form::script::FieldActions {
keystroke: source_of(AActionType::KeyStroke),
validate: source_of(AActionType::Validate),
calculate: source_of(AActionType::Calculate),
format: source_of(AActionType::Format),
mouse_enter: source_of(AActionType::CursorEnter),
mouse_exit: source_of(AActionType::CursorExit),
mouse_down: source_of(AActionType::ButtonDown),
mouse_up: source_of(AActionType::ButtonUp),
focus: source_of(AActionType::GetFocus),
blur: source_of(AActionType::LoseFocus),
};
if actions == pdfrum_form::script::FieldActions::default() {
continue;
}
let Some(index) = widget.field_index else {
continue;
};
let text_like = matches!(
widget.kind,
Some(pdfrum_doc::form::FieldKind::Text | pdfrum_doc::form::FieldKind::Combo)
);
cascade.set_field(index, widget.name.clone(), widget.value(resolve), actions);
if text_like {
loaded.push(pdfrum_form::FieldRef {
name: widget.name.clone(),
index: Some(index),
});
}
}
for field in loaded {
cascade.format_on_load(&field);
}
}
#[cfg(not(feature = "javascript"))]
#[expect(
clippy::unused_self,
reason = "the feature-on twin takes `&mut self`; one signature, two bodies"
)]
fn install_page_scripts(&mut self, _page: PageIndex) {}
fn read_page(&self, page: PageIndex) -> Option<pdfrum_form::PageForm> {
let loaded = self.doc.page(page).ok()?;
Some(pdfrum_form::read_page(
page,
&loaded.dict.dict,
&self.doc.catalog(),
self.doc.parser(),
))
}
fn permissions(&self) -> pdfrum_form::Permissions {
if !self.doc.is_encrypted() {
return pdfrum_form::Permissions::ALL;
}
let granted = self.doc.permissions();
pdfrum_form::Permissions {
fill_form: granted.fill_form,
modify_annotation: granted.annotate,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn document() -> Document {
match Document::open("tests/fixtures/text_form.pdf") {
Ok(doc) => doc,
Err(error) => panic!("the text_form fixture must open: {error}"),
}
}
#[test]
fn a_fresh_session_has_no_focus() {
let doc = document();
let session = FormSession::new(&doc);
assert!(session.focused_annot().is_none());
assert!(session.focused_text().is_none());
assert!(!session.can_undo());
assert!(!session.can_redo());
}
#[test]
fn the_default_configuration_is_this_platforms() {
let doc = document();
let session = FormSession::new(&doc);
assert_eq!(session.config().max_undo_items, 10_000);
assert!(!session.config().focusable.is_empty());
}
#[test]
fn killing_focus_from_nothing_is_harmless() {
let doc = document();
let mut session = FormSession::new(&doc);
let response = session.blur();
assert!(!response.consumed);
assert!(response.updates.is_empty());
}
#[test]
fn applying_an_event_routes_where_the_wrapper_does() {
let doc = document();
let mut through_event = FormSession::new(&doc);
let inside = kurbo::Point::new(120.0, 115.0);
through_event.apply(Event::MouseDown {
button: Button::Left,
at: inside,
modifiers: Modifiers::NONE,
});
let mut through_wrapper = FormSession::new(&doc);
through_wrapper.mouse_down(0, inside, Modifiers::NONE);
assert_eq!(
through_event.focused_annot(),
through_wrapper.focused_annot()
);
assert!(through_event.focused_annot().is_some());
}
#[test]
fn applying_a_key_needs_no_page() {
let doc = document();
let mut session = FormSession::new(&doc);
let response = session.apply(Event::KeyDown {
key: Key::Tab,
modifiers: Modifiers::NONE,
});
assert!(response.consumed);
assert!(session.focused_annot().is_some());
}
#[test]
fn queries_without_focus_answer_rather_than_panic() {
let doc = document();
let mut session = FormSession::new(&doc);
assert!(!session.is_index_selected(0));
assert!(!session.set_index_selected(0, true));
assert!(!session.is_index_selected(usize::MAX));
}
}
#[cfg(feature = "javascript")]
mod open_requests {
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_doc::nav::{Action, ActionKind, NameTree};
use pdfrum_object::{Dict, Name, Object, names};
pub enum Request {
Script { whence: String, source: String },
Named(String),
}
pub fn name_tree_scripts(
catalog: &Dict,
r: &pdfrum_parser::Document,
found: &mut Vec<Request>,
) {
let limits = Limits::default();
let mut diags = Diagnostics::default();
let Some(tree) = NameTree::open(catalog, names::JAVA_SCRIPT, r) else {
return;
};
for index in 0..tree.count(r, &limits, &mut diags) {
let Some((name, Object::Dict(dict))) =
tree.lookup_by_index(index, r, &limits, &mut diags)
else {
continue;
};
let action = Action::new(dict);
if action.kind() != ActionKind::JavaScript {
continue;
}
if let Some(source) = action.javascript(r).filter(|s| !s.is_empty()) {
found.push(Request::Script {
whence: name,
source,
});
}
}
}
pub fn open_action(catalog: &Dict, r: &pdfrum_parser::Document, found: &mut Vec<Request>) {
let limits = Limits::default();
let mut diags = Diagnostics::default();
let Some(dict) = catalog.dict(&Name::new(*b"OpenAction"), r) else {
return;
};
let root = Action::new(dict);
for action in std::iter::once(root.clone()).chain(root.chain(r, &limits, &mut diags)) {
match action.kind() {
ActionKind::JavaScript => {
if let Some(source) = action.javascript(r).filter(|s| !s.is_empty()) {
found.push(Request::Script {
whence: String::new(),
source,
});
}
}
ActionKind::Named => {
let name = String::from_utf8_lossy(&action.named_action(r)).into_owned();
found.push(Request::Named(name));
}
_ => {}
}
}
}
}