use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{ByteSpan, Dict, Resolve, Stream};
use pdfrum_page::{BuildContext, Page, Resources, build_form_object};
use crate::annot::appearance::{ApMode, annot_ap, annot_matrix};
use crate::annot::{AnnotList, Annotation, Subtype};
use crate::ap;
use crate::names;
#[cfg(feature = "profiling")]
mod profile {
pub use pdfrum_page::renderprofile::{Stage, stage};
}
#[cfg(not(feature = "profiling"))]
mod profile {
#[derive(Debug, Clone, Copy)]
pub enum Stage {
AnnotList,
FormFonts,
GenerateAppearances,
OpenAction,
AnnotLoop,
}
#[inline]
pub fn stage<T>(_stage: Stage, body: impl FnOnce() -> T) -> T {
body()
}
}
use profile::{Stage, stage};
pub fn overlay<R: Resolve>(
page: &mut Page,
page_dict: &Dict,
catalog: &Dict,
r: &R,
ctx: &mut BuildContext,
limits: &Limits,
diags: &mut Diagnostics,
) {
overlay_with(page, page_dict, catalog, r, ctx, limits, diags, None);
}
#[expect(
clippy::too_many_arguments,
reason = "the pass reads six independent inputs plus its two sinks; \
bundling them into a context struct is the god-object shape \
STYLE §1 forbids"
)]
pub fn overlay_with<R: Resolve>(
page: &mut Page,
page_dict: &Dict,
catalog: &Dict,
r: &R,
ctx: &mut BuildContext,
limits: &Limits,
diags: &mut Diagnostics,
supplied: Option<&ap::AnnotOverlay>,
) {
#[expect(
clippy::cast_possible_truncation,
reason = "a page width beyond f32 has already lost meaning, and the \
value only places a synthesized pop-up, which never paints"
)]
let page_width = page.crop_box.width() as f32;
let list = stage(Stage::AnnotList, || {
AnnotList::load(page_dict, page_width, r)
});
let resources = Resources::for_page(page.resources.clone());
let needs_fonts = !list.popups.is_empty()
|| list
.annots
.iter()
.any(|annot| matches!(annot.subtype, Subtype::Widget | Subtype::FreeText));
let fonts =
needs_fonts.then(|| stage(Stage::FormFonts, || ap::FormFonts::load(catalog, r, ctx)));
let mut generated = stage(Stage::GenerateAppearances, || {
ap::generate_appearances_with_text(page_dict, catalog, fonts.as_deref(), r, diags)
});
if let Some(supplied) = supplied {
generated.merge_over(supplied);
}
let hidden = stage(Stage::OpenAction, || {
crate::nav::hidden_by_open_action(catalog, r, limits, diags)
});
let focus = generated.focus();
stage(Stage::AnnotLoop, || {
for (slot, annot) in list.annots.iter().enumerate() {
let flags = hidden.flags(&annot.dict, r);
if !is_visible(annot.subtype, flags) {
continue;
}
let index = list.source_indices.get(slot).copied().unwrap_or(slot);
if matches!(generated.appearance(index), ap::Appearance::Suppressed) {
push_chrome(page, annot, index, focus, r, limits, diags);
continue;
}
if generated.get(index).is_none()
&& let Some(object) = invalid_outline(annot, r)
{
page.objects.push(object);
push_chrome(page, annot, index, focus, r, limits, diags);
continue;
}
let (form, placed) = if let Some(made) = generated.get(index) {
let mut placed = annot.clone();
placed.rect = generated.rect(index, annot.rect_for_drawing(true));
(
Stream::new(ap::stream_dict(made), ByteSpan::from(made.stream.clone())),
placed,
)
} else {
let Some(form) = annot_ap(&annot.dict, ApMode::Normal, false, r) else {
push_chrome(page, annot, index, focus, r, limits, diags);
continue;
};
(form, annot.clone())
};
let matrix = annot_matrix(&placed, &form.dict, 0, kurbo::Affine::IDENTITY, r);
if !matrix.as_coeffs().iter().all(|c| c.is_finite()) {
continue;
}
let live_edit = supplied.is_some_and(|overlay| overlay.is_live_edit(index));
if let Some(object) = pdfrum_page::build_form_object_with(
&form, matrix, &resources, r, ctx, limits, diags, live_edit,
) {
page.objects.push(object);
}
push_chrome(page, annot, index, focus, r, limits, diags);
}
if let Some(fonts) = &fonts {
push_open_popup(
page,
&list,
generated.hover(),
fonts,
&resources,
r,
ctx,
limits,
diags,
);
}
});
}
#[expect(
clippy::too_many_arguments,
reason = "the same six inputs plus two sinks the pass itself carries; \
see `overlay_with`"
)]
fn push_open_popup<R: Resolve>(
page: &mut Page,
list: &AnnotList,
hover: Option<usize>,
fonts: &ap::FormFonts,
resources: &Resources,
r: &R,
ctx: &mut BuildContext,
limits: &Limits,
diags: &mut Diagnostics,
) {
let Some(hover) = hover else {
return;
};
let Some(slot) = list.source_indices.iter().position(|&index| index == hover) else {
return;
};
let Some((_, popup)) = list.popups.iter().find(|(parent, _)| *parent == slot) else {
return;
};
let Some(font) = fonts.face(b"") else {
return;
};
let width = |code: u32| ap::TextFont::char_width(font, code);
let metrics = ap::TextFont::metrics_of(font, &width);
let encode = |code: u32| {
ap::TextFont {
font,
metrics: ap::TextFont::metrics_of(font, &width),
}
.encode(code)
};
let Some(made) = ap::popup::popup(&popup.dict, &metrics, &encode, r) else {
return;
};
diags.record(
pdfrum_common::Severity::Recovered,
pdfrum_common::DiagKind::AppearanceGenerated,
None,
);
let generated = ap::GeneratedAp {
stream: made.stream,
bbox: popup.rect,
matrix: kurbo::Affine::IDENTITY,
resources: ap::resources_dict(
ap::ext_gstate_dict(&popup.dict, false, r),
made.font_resources,
),
rect_override: None,
as_override: None,
};
let form = Stream::new(
ap::stream_dict(&generated),
ByteSpan::from(generated.stream.clone()),
);
let matrix = annot_matrix(popup, &form.dict, 0, kurbo::Affine::IDENTITY, r);
if !matrix.as_coeffs().iter().all(|c| c.is_finite()) {
return;
}
if let Some(object) = build_form_object(&form, matrix, resources, r, ctx, limits, diags) {
page.objects.push(object);
}
}
fn push_chrome<R: Resolve>(
page: &mut Page,
annot: &Annotation,
index: usize,
focus: Option<ap::Focus>,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) {
if let Some(focus) = focus.filter(|focus| focus.annot == index) {
if let Some(object) = focus_rect(annot, focus.box_) {
page.objects.push(object);
}
return;
}
if let Some(object) = highlight(annot, r, limits, diags) {
page.objects.push(object);
}
}
#[must_use]
fn focus_rect(annot: &Annotation, box_: ap::FocusBox) -> Option<pdfrum_page::PageObject> {
let rect = match box_ {
ap::FocusBox::None => return None,
ap::FocusBox::Rect(rect) => rect,
ap::FocusBox::Inflated => normalized(annot.rect).inflate(1.0, 1.0),
};
let rect = normalized(rect);
if rect.width() <= 0.0 || rect.height() <= 0.0 {
return None;
}
let mut stroke = pdfrum_page::ColorValue::default();
stroke.set_space(std::sync::Arc::new(pdfrum_page::ColorSpace::DeviceRgb));
let _ = stroke.set_components(&[0.0, 0.0, 0.0]);
let state = pdfrum_page::GraphicsState {
stroke,
stroke_params: pdfrum_page::StrokeParams {
width: 1.0,
dash: [1.0].into_iter().collect(),
dash_phase: 0.0,
..pdfrum_page::StrokeParams::default()
},
..pdfrum_page::GraphicsState::default()
};
Some(pdfrum_page::PageObject::Path(Box::new(
pdfrum_page::Content {
object: pdfrum_page::PathObject {
path: kurbo::Shape::to_path(&rect, 0.1),
matrix: kurbo::Affine::IDENTITY,
fill_rule: pdfrum_page::FillRule::None,
stroke: true,
},
state,
marks: pdfrum_page::ContentMarks::default(),
content_stream: None,
dirty: false,
active: true,
},
)))
}
fn normalized(rect: kurbo::Rect) -> kurbo::Rect {
kurbo::Rect::new(
rect.x0.min(rect.x1),
rect.y0.min(rect.y1),
rect.x0.max(rect.x1),
rect.y0.max(rect.y1),
)
}
fn invalid_outline<R: Resolve>(annot: &Annotation, r: &R) -> Option<pdfrum_page::PageObject> {
const OUTLINE_GREY: f32 = 0xAA_u8 as f32 / 255.0;
if annot.subtype != Subtype::Widget {
return None;
}
let (limits, mut diags) = (Limits::default(), Diagnostics::default());
let flags = crate::form::FieldFlags::from_bits(
crate::form::attr::field_attr(&annot.dict, names::FF, r, &limits, &mut diags)
.and_then(|value| value.as_int())
.unwrap_or(0),
);
let field_type = crate::form::attr::field_attr(&annot.dict, names::FT, r, &limits, &mut diags)
.map(|value| value.to_byte_string())
.unwrap_or_default();
if !matches!(
crate::form::FieldKind::classify(&field_type, flags),
Some(crate::form::FieldKind::Check | crate::form::FieldKind::Radio)
) {
return None;
}
if state_appearance_resolves(&annot.dict, r) {
return None;
}
let rect = normalized(annot.rect);
let mut stroke = pdfrum_page::ColorValue::default();
stroke.set_space(std::sync::Arc::new(pdfrum_page::ColorSpace::DeviceRgb));
let _ = stroke.set_components(&[OUTLINE_GREY, OUTLINE_GREY, OUTLINE_GREY]);
let state = pdfrum_page::GraphicsState {
stroke,
stroke_params: pdfrum_page::StrokeParams {
width: 0.0,
..pdfrum_page::StrokeParams::default()
},
..pdfrum_page::GraphicsState::default()
};
Some(pdfrum_page::PageObject::Path(Box::new(
pdfrum_page::Content {
object: pdfrum_page::PathObject {
path: kurbo::Shape::to_path(&rect, 0.1),
matrix: kurbo::Affine::IDENTITY,
fill_rule: pdfrum_page::FillRule::None,
stroke: true,
},
state,
marks: pdfrum_page::ContentMarks::default(),
content_stream: None,
dirty: false,
active: true,
},
)))
}
fn state_appearance_resolves<R: Resolve>(dict: &Dict, r: &R) -> bool {
let Some(sub) = dict
.dict(names::AP, r)
.and_then(|ap| ap.get(names::N, r).map(|value| value.get().clone()))
else {
return false;
};
let Some(states) = sub.as_dict() else {
return matches!(sub, pdfrum_object::Object::Stream(_));
};
let state = dict.byte_string(names::AS, r).unwrap_or_default();
states.stream(&pdfrum_object::Name::new(state), r).is_some()
}
fn highlight<R: Resolve>(
annot: &Annotation,
r: &R,
limits: &Limits,
diags: &mut Diagnostics,
) -> Option<pdfrum_page::PageObject> {
if annot.subtype != Subtype::Widget {
return None;
}
let field_type = crate::form::attr::field_attr(&annot.dict, names::FT, r, limits, diags)
.map(|value| value.to_byte_string())
.unwrap_or_default();
let flags = crate::form::FieldFlags::from_bits(
crate::form::attr::field_attr(&annot.dict, names::FF, r, limits, diags)
.and_then(|value| value.as_int())
.unwrap_or(0),
);
let kind = crate::form::FieldKind::classify(&field_type, flags)?;
if flags.is_read_only()
|| matches!(
kind,
crate::form::FieldKind::Button | crate::form::FieldKind::Signature
)
{
return None;
}
let rect = normalized(annot.rect);
if rect.width() <= 0.0 || rect.height() <= 0.0 {
return None;
}
Some(pdfrum_page::PageObject::Path(Box::new(
pdfrum_page::Content {
object: pdfrum_page::PathObject {
path: kurbo::Shape::to_path(&rect, 0.1),
matrix: kurbo::Affine::IDENTITY,
fill_rule: pdfrum_page::FillRule::Winding,
stroke: false,
},
state: highlight_state(),
marks: pdfrum_page::ContentMarks::default(),
content_stream: None,
dirty: false,
active: true,
},
)))
}
fn highlight_state() -> pdfrum_page::GraphicsState {
const HIGHLIGHT_BGR: u32 = 0x00FF_E4DD;
const HIGHLIGHT_ALPHA: f32 = 100.0 / 255.0;
let channel =
|shift: u32| u8::try_from((HIGHLIGHT_BGR >> shift) & 0xff).map_or(0.0, f32::from) / 255.0;
let mut fill = pdfrum_page::ColorValue::default();
fill.set_space(std::sync::Arc::new(pdfrum_page::ColorSpace::DeviceRgb));
let _ = fill.set_components(&[channel(0), channel(8), channel(16)]);
pdfrum_page::GraphicsState {
fill,
general: pdfrum_page::GeneralState {
fill_alpha: HIGHLIGHT_ALPHA,
..pdfrum_page::GeneralState::default()
},
..pdfrum_page::GraphicsState::default()
}
}
fn is_visible(subtype: Subtype, flags: crate::annot::AnnotFlags) -> bool {
if subtype == Subtype::Popup {
return false;
}
if flags.is_hidden() || flags.no_view() {
return false;
}
if subtype == Subtype::Widget && flags.contains(crate::annot::AnnotFlags::INVISIBLE) {
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::{focus_rect, highlight, highlight_state, invalid_outline, is_visible, push_chrome};
use crate::annot::{AnnotFlags, Annotation, Subtype};
use crate::ap;
use pdfrum_common::{Diagnostics, Limits};
use pdfrum_object::{Dict, Name, NoResolve, Object};
use pdfrum_page::PageObject;
fn annot(subtype: Subtype, flags: i64) -> Annotation {
let mut annot = Annotation::read(&Dict::new(), &NoResolve);
annot.flags = AnnotFlags::from_bits(flags);
annot.subtype = subtype;
annot
}
fn widget(field_type: &str, ff: i64) -> Annotation {
let dict = Dict::from_pairs([
(Name::from("Subtype"), Object::Name(Name::from("Widget"))),
(Name::from("FT"), Object::Name(Name::from(field_type))),
(Name::from("Ff"), Object::Int(ff)),
(
Name::from("Rect"),
Object::Array(pdfrum_object::Array::of([
Object::Int(100),
Object::Int(100),
Object::Int(200),
Object::Int(130),
])),
),
]);
Annotation::read(&dict, &NoResolve)
}
fn tinted(annot: &Annotation) -> bool {
let (limits, mut diags) = (Limits::default(), Diagnostics::default());
highlight(annot, &NoResolve, &limits, &mut diags).is_some()
}
#[test]
fn the_highlight_colour_is_bgr_and_composites_to_the_goldens_tint() {
let state = highlight_state();
let rgb = state.fill.to_rgb().expect("a resolved colour");
assert_eq!(rgb.to_bytes(), [0xDD, 0xE4, 0xFF]);
assert!((state.general.fill_alpha * 255.0 - 100.0).abs() < 1e-4);
let alpha = 100;
let over_white = |c: i32| ((255 * (255 - alpha)) + c * alpha) / 255;
assert_eq!(
[over_white(0xDD), over_white(0xE4), over_white(0xFF)],
[241, 244, 255]
);
}
#[test]
fn every_fillable_field_type_is_tinted() {
for ft in ["Tx", "Ch"] {
assert!(tinted(&widget(ft, 0)), "{ft}");
}
assert!(tinted(&widget("Btn", 0)));
assert!(tinted(&widget("Btn", 1 << 15)), "radio");
}
#[test]
fn the_three_kinds_of_field_that_are_never_tinted() {
assert!(!tinted(&widget("Btn", 1 << 16)));
assert!(!tinted(&widget("Sig", 0)));
assert!(!tinted(&widget("Tx", 1)));
let mut not_read_only = widget("Tx", 0);
not_read_only.flags = AnnotFlags::from_bits(64);
assert!(
tinted(¬_read_only),
"the annotation's ReadOnly bit is a different flag word"
);
}
#[test]
fn a_widget_with_no_field_type_is_not_tinted() {
let bare = Dict::from_pairs([(Name::from("Subtype"), Object::Name(Name::from("Widget")))]);
assert!(!tinted(&Annotation::read(&bare, &NoResolve)));
assert!(!tinted(&annot(Subtype::Square, 0)));
}
fn stateful(field_type: &str, ff: i64, as_: &str, states: &[&str]) -> Annotation {
let normal = Dict::from_pairs(states.iter().map(|state| {
(
Name::from(*state),
Object::Stream(Box::new(pdfrum_object::Stream::new(
Dict::new(),
pdfrum_object::ByteSpan::from(b"x".to_vec()),
))),
)
}));
let mut annot = widget(field_type, ff);
let mut dict = annot.dict.clone();
dict.push(
Name::from("AP"),
Object::Dict(Dict::from_pairs([(Name::from("N"), Object::Dict(normal))])),
);
dict.push(Name::from("AS"), Object::Name(Name::from(as_)));
annot.dict = dict;
annot
}
fn outlined(annot: &Annotation) -> bool {
invalid_outline(annot, &NoResolve).is_some()
}
#[test]
fn a_state_with_no_stream_outlines_a_checkbox_and_a_radio() {
assert!(outlined(&stateful("Btn", 0, "Off", &["Yes"])), "checkbox");
assert!(
outlined(&stateful("Btn", 1 << 15, "Off", &["value1"])),
"radio"
);
assert!(!outlined(&stateful("Btn", 0, "Yes", &["Yes", "Off"])));
}
#[test]
fn only_a_checkbox_or_a_radio_is_ever_outlined() {
for (ft, ff) in [("Tx", 0), ("Ch", 0), ("Btn", 1 << 16), ("Sig", 0)] {
assert!(!outlined(&stateful(ft, ff, "Off", &["Yes"])), "{ft}");
}
assert!(!outlined(&annot(Subtype::Square, 0)));
}
#[test]
fn the_state_is_read_from_as_alone() {
let with_state = stateful("Btn", 0, "Off", &["Off"]);
assert!(!outlined(&with_state), "an `Off` stream resolves");
let mut no_state = with_state.clone();
no_state.dict = Dict::from_pairs(
with_state
.dict
.keys()
.filter(|key| key.as_bytes() != b"AS")
.filter_map(|key| {
with_state
.dict
.get(key, &NoResolve)
.map(|value| (key.clone(), value.get().clone()))
})
.collect::<Vec<_>>(),
);
assert!(outlined(&no_state), "with no `/AS` nothing resolves");
}
#[test]
fn the_outline_is_a_hairline_grey_stroke_and_fills_nothing() {
let object = invalid_outline(&stateful("Btn", 0, "Off", &["Yes"]), &NoResolve)
.expect("an invalid checkbox");
let pdfrum_page::PageObject::Path(path) = object else {
panic!("a path")
};
assert!(path.object.stroke);
assert_eq!(path.object.fill_rule, pdfrum_page::FillRule::None);
assert!(path.state.stroke_params.width.abs() < f32::EPSILON);
assert_eq!(
path.state
.stroke
.to_rgb()
.expect("a resolved colour")
.to_bytes(),
[0xAA, 0xAA, 0xAA]
);
}
fn visible(subtype: Subtype, flags: i64) -> bool {
is_visible(subtype, AnnotFlags::from_bits(flags))
}
#[test]
fn hidden_and_noview_suppress_in_both_passes_and_print_does_not() {
for subtype in [Subtype::Widget, Subtype::Square] {
assert!(visible(subtype, 0), "{subtype:?}");
assert!(visible(subtype, 4), "Print alone still shows");
assert!(!visible(subtype, 2), "Hidden");
assert!(!visible(subtype, 32), "NoView");
assert!(!visible(subtype, 4 | 32), "NoView beats Print");
}
}
#[test]
fn invisible_suppresses_a_widget_and_only_a_widget() {
assert!(!visible(Subtype::Widget, 1));
assert!(visible(Subtype::Square, 1));
}
#[test]
fn a_popup_is_never_painted() {
assert!(!visible(Subtype::Popup, 0));
}
fn chrome(annot: &Annotation, index: usize, focus: Option<ap::Focus>) -> Vec<PageObject> {
let (limits, mut diags) = (Limits::default(), Diagnostics::default());
let mut page = pdfrum_page::Page::empty();
push_chrome(
&mut page, annot, index, focus, &NoResolve, &limits, &mut diags,
);
page.objects
}
fn only_path(objects: &[PageObject]) -> &pdfrum_page::Content<pdfrum_page::PathObject> {
match objects {
[PageObject::Path(path)] => path,
_ => panic!("exactly one path"),
}
}
#[test]
fn the_focused_annotation_loses_its_tint() {
let widget = widget("Tx", 0);
assert_eq!(chrome(&widget, 3, None).len(), 1, "unfocused: a tint");
assert!(
chrome(&widget, 3, Some(ap::Focus::at(3))).is_empty(),
"focused with no focus box: nothing at all"
);
}
#[test]
fn a_focus_box_of_none_strokes_nothing_but_still_suppresses_the_tint() {
let widget = widget("Tx", 0);
let focused = ap::Focus {
annot: 0,
box_: ap::FocusBox::None,
};
assert!(chrome(&widget, 0, Some(focused)).is_empty());
assert_eq!(focus_rect(&widget, ap::FocusBox::None), None);
}
#[test]
fn a_focused_widget_strokes_a_dashed_black_hairline_over_its_focus_box() {
let widget = widget("Tx", 0);
let box_ = kurbo::Rect::new(101.0, 402.0, 186.0, 416.0);
let objects = chrome(
&widget,
0,
Some(ap::Focus {
annot: 0,
box_: ap::FocusBox::Rect(box_),
}),
);
let path = only_path(&objects);
assert!(path.object.stroke);
assert_eq!(path.object.fill_rule, pdfrum_page::FillRule::None);
assert_eq!(
path.state.stroke.to_rgb().expect("a colour").to_bytes(),
[0, 0, 0]
);
assert!((path.state.stroke_params.width - 1.0).abs() < f32::EPSILON);
assert_eq!(path.state.stroke_params.dash.as_slice(), [1.0]);
assert!(path.state.stroke_params.dash_phase.abs() < f32::EPSILON);
assert_eq!(path.object.matrix, kurbo::Affine::IDENTITY);
assert_eq!(kurbo::Shape::bounding_box(&path.object.path), box_);
assert!(!path.dirty, "annotation chrome is not page content");
}
#[test]
fn the_list_box_focus_box_is_the_caret_item_not_the_widget_rect() {
let mut listbox = widget("Ch", 1 << 21);
listbox.rect = kurbo::Rect::new(100.0, 400.0, 200.0, 430.0);
let caret_item = kurbo::Rect::new(101.0, 401.0, 186.0, 415.0);
let objects = chrome(
&listbox,
0,
Some(ap::Focus {
annot: 0,
box_: ap::FocusBox::Rect(caret_item),
}),
);
let bounds = kurbo::Shape::bounding_box(&only_path(&objects).object.path);
assert_eq!(bounds, caret_item);
assert!((bounds.height() - 14.0).abs() < f64::EPSILON);
assert!(bounds.width() < listbox.rect.width());
}
#[test]
fn an_inflated_focus_box_grows_the_annotation_rect_by_one_unit() {
let mut check = widget("Btn", 0);
check.rect = kurbo::Rect::new(100.0, 100.0, 200.0, 130.0);
let objects = chrome(
&check,
0,
Some(ap::Focus {
annot: 0,
box_: ap::FocusBox::Inflated,
}),
);
assert_eq!(
kurbo::Shape::bounding_box(&only_path(&objects).object.path),
kurbo::Rect::new(99.0, 99.0, 201.0, 131.0)
);
let mut backwards = check.clone();
backwards.rect = kurbo::Rect::new(200.0, 130.0, 100.0, 100.0);
let objects = chrome(
&backwards,
0,
Some(ap::Focus {
annot: 0,
box_: ap::FocusBox::Inflated,
}),
);
assert_eq!(
kurbo::Shape::bounding_box(&only_path(&objects).object.path),
kurbo::Rect::new(99.0, 99.0, 201.0, 131.0)
);
}
#[test]
fn an_empty_focus_box_strokes_nothing_and_does_not_bring_the_tint_back() {
let widget = widget("Tx", 0);
for degenerate in [
kurbo::Rect::new(100.0, 100.0, 100.0, 130.0),
kurbo::Rect::new(100.0, 100.0, 200.0, 100.0),
] {
assert_eq!(focus_rect(&widget, ap::FocusBox::Rect(degenerate)), None);
assert!(
chrome(
&widget,
0,
Some(ap::Focus {
annot: 0,
box_: ap::FocusBox::Rect(degenerate)
})
)
.is_empty()
);
}
}
#[test]
fn every_index_but_the_focused_one_is_unchanged() {
let widgets = [
widget("Tx", 0),
widget("Ch", 0),
widget("Btn", 0),
widget("Btn", 1 << 16),
widget("Sig", 0),
widget("Tx", 1),
];
let focused = ap::Focus {
annot: 1,
box_: ap::FocusBox::Inflated,
};
for (index, annot) in widgets.iter().enumerate() {
let before = chrome(annot, index, None);
let after = chrome(annot, index, Some(focused));
if index == focused.annot {
assert_ne!(before, after, "the focused index does change");
continue;
}
assert_eq!(before, after, "index {index} moved");
}
}
#[test]
fn no_focus_is_the_pass_as_it_was() {
let (limits, mut diags) = (Limits::default(), Diagnostics::default());
for annot in [
widget("Tx", 0),
widget("Ch", 0),
widget("Btn", 0),
widget("Btn", 1 << 15),
widget("Btn", 1 << 16),
widget("Sig", 0),
widget("Tx", 1),
annot(Subtype::Square, 0),
] {
let expected: Vec<PageObject> = highlight(&annot, &NoResolve, &limits, &mut diags)
.into_iter()
.collect();
for index in 0..3 {
assert_eq!(chrome(&annot, index, None), expected, "index {index}");
}
}
}
#[test]
fn focus_merges_over_and_is_not_bounded_by_the_overlay() {
let mut base = ap::AnnotOverlay::with_capacity(4);
assert_eq!(base.focus(), None);
let mut supplied = ap::AnnotOverlay::with_capacity(1);
supplied.set_focus(ap::Focus::at(9));
base.merge_over(&supplied);
assert_eq!(base.focus(), Some(ap::Focus::at(9)));
base.merge_over(&ap::AnnotOverlay::with_capacity(4));
assert_eq!(base.focus(), Some(ap::Focus::at(9)));
}
}