use pdfrum_doc::GeneratedAp;
use pdfrum_doc::nav::Action;
use crate::event::Modifiers;
use crate::session::AnnotId;
#[derive(Debug, Clone, PartialEq)]
pub struct AppearanceUpdate {
pub annot: AnnotId,
pub kind: UpdateKind,
}
impl AppearanceUpdate {
#[must_use]
pub fn new(annot: AnnotId, kind: UpdateKind) -> AppearanceUpdate {
AppearanceUpdate { annot, kind }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateKind {
Regenerated(Box<GeneratedAp>),
LiveEdit(Box<GeneratedAp>),
RevertedToFileAppearance,
ActionRequested {
action: Box<Action>,
modifiers: Modifiers,
},
FocusChanged {
from: Option<AnnotId>,
to: Option<AnnotId>,
},
}
impl UpdateKind {
#[must_use]
pub fn appearance(&self) -> Option<&GeneratedAp> {
match self {
UpdateKind::Regenerated(ap) | UpdateKind::LiveEdit(ap) => Some(ap),
UpdateKind::RevertedToFileAppearance
| UpdateKind::ActionRequested { .. }
| UpdateKind::FocusChanged { .. } => None,
}
}
#[must_use]
pub fn is_live_edit(&self) -> bool {
matches!(self, UpdateKind::LiveEdit(_))
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Response {
pub consumed: bool,
pub updates: Vec<AppearanceUpdate>,
}
impl Response {
#[must_use]
pub fn ignored() -> Response {
Response {
consumed: false,
updates: Vec::new(),
}
}
#[must_use]
pub fn consumed() -> Response {
Response {
consumed: true,
updates: Vec::new(),
}
}
#[must_use]
pub fn with(updates: Vec<AppearanceUpdate>) -> Response {
Response {
consumed: true,
updates,
}
}
pub fn push(&mut self, update: AppearanceUpdate) {
self.updates.push(update);
}
pub fn absorb(&mut self, other: Response) {
self.consumed |= other.consumed;
self.updates.extend(other.updates);
}
pub fn actions(&self) -> impl Iterator<Item = (&Action, Modifiers)> {
self.updates.iter().filter_map(|u| match &u.kind {
UpdateKind::ActionRequested { action, modifiers } => {
Some((action.as_ref(), *modifiers))
}
UpdateKind::Regenerated(_)
| UpdateKind::LiveEdit(_)
| UpdateKind::RevertedToFileAppearance
| UpdateKind::FocusChanged { .. } => None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::session::AnnotId;
use pdfrum_object::Dict;
fn annot() -> AnnotId {
AnnotId::new(0, 3)
}
fn action() -> Action {
Action::new(Dict::new())
}
fn appearance() -> GeneratedAp {
GeneratedAp {
stream: Vec::new(),
bbox: kurbo::Rect::ZERO,
matrix: kurbo::Affine::IDENTITY,
resources: Dict::new(),
rect_override: None,
as_override: None,
}
}
#[test]
fn an_event_can_be_consumed_without_changing_anything() {
let response = Response::consumed();
assert!(response.consumed);
assert!(response.updates.is_empty());
let ignored = Response::ignored();
assert!(!ignored.consumed);
assert!(ignored.updates.is_empty());
}
#[test]
fn folding_responses_keeps_both_orders() {
let mut first = Response::with(vec![AppearanceUpdate::new(
annot(),
UpdateKind::RevertedToFileAppearance,
)]);
let second = Response::with(vec![AppearanceUpdate::new(
AnnotId::new(0, 4),
UpdateKind::FocusChanged {
from: None,
to: Some(AnnotId::new(0, 4)),
},
)]);
first.absorb(second);
let order: Vec<AnnotId> = first.updates.iter().map(|u| u.annot).collect();
assert_eq!(order, vec![annot(), AnnotId::new(0, 4)]);
}
#[test]
fn absorbing_an_ignored_response_does_not_un_consume() {
let mut consumed = Response::consumed();
consumed.absorb(Response::ignored());
assert!(consumed.consumed);
let mut ignored = Response::ignored();
ignored.absorb(Response::consumed());
assert!(ignored.consumed, "either half consuming is enough");
}
#[test]
fn actions_come_back_in_order_with_their_modifiers() {
let mut response = Response::default();
for modifiers in [
Modifiers::NONE,
Modifiers::CONTROL,
Modifiers::SHIFT,
Modifiers::SHIFT | Modifiers::CONTROL,
] {
response.push(AppearanceUpdate::new(
annot(),
UpdateKind::ActionRequested {
action: Box::new(action()),
modifiers,
},
));
}
let seen: Vec<u32> = response.actions().map(|(_, m)| m.bits()).collect();
assert_eq!(seen, vec![0, 2, 1, 3]);
}
#[test]
fn a_response_with_no_actions_yields_none() {
let response = Response::with(vec![AppearanceUpdate::new(
annot(),
UpdateKind::RevertedToFileAppearance,
)]);
assert_eq!(response.actions().count(), 0);
}
#[test]
fn reverting_carries_no_appearance_of_its_own() {
let reverted = UpdateKind::RevertedToFileAppearance;
assert!(reverted.appearance().is_none());
assert!(!reverted.is_live_edit());
let generated = UpdateKind::Regenerated(Box::new(appearance()));
assert_ne!(reverted, generated);
assert!(generated.appearance().is_some());
}
#[test]
fn the_two_appearance_paths_are_distinguishable() {
let live = UpdateKind::LiveEdit(Box::new(appearance()));
let regenerated = UpdateKind::Regenerated(Box::new(appearance()));
assert!(live.is_live_edit());
assert!(!regenerated.is_live_edit());
assert!(live.appearance().is_some());
assert!(regenerated.appearance().is_some());
assert!(UpdateKind::RevertedToFileAppearance.appearance().is_none());
}
}