use std::collections::BTreeSet;
use crate::page::{Page, PageObject};
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("page object index {index} is out of range (len {len})")]
pub struct IndexOutOfRange {
pub index: usize,
pub len: usize,
}
impl PageObject {
#[must_use]
pub fn is_dirty(&self) -> bool {
self.common().dirty
}
#[must_use]
pub fn is_active(&self) -> bool {
self.common().active
}
#[must_use]
pub fn content_stream(&self) -> Option<usize> {
self.common().content_stream
}
pub fn mark_dirty(&mut self) {
*self.common_mut().dirty = true;
}
pub fn mark_clean(&mut self) {
*self.common_mut().dirty = false;
}
pub fn set_active(&mut self, active: bool) {
let common = self.common_mut();
if *common.active == active {
return;
}
*common.active = active;
*common.dirty = true;
}
pub fn set_content_stream(&mut self, stream: Option<usize>) {
*self.common_mut().content_stream = stream;
}
}
impl Page {
#[must_use]
pub fn objects(&self) -> &[PageObject] {
&self.objects
}
pub fn active_objects(&self) -> impl Iterator<Item = &PageObject> {
self.objects.iter().filter(|o| o.is_active())
}
pub fn object_mut(&mut self, index: usize) -> Option<&mut PageObject> {
let object = self.objects.get_mut(index)?;
object.mark_dirty();
Some(object)
}
pub fn push_object(&mut self, mut object: PageObject) {
object.mark_dirty();
object.set_content_stream(None);
self.objects.push(object);
}
pub fn insert_object(
&mut self,
index: usize,
mut object: PageObject,
) -> Result<(), IndexOutOfRange> {
let len = self.objects.len();
if index > len {
return Err(IndexOutOfRange { index, len });
}
object.mark_dirty();
if object.content_stream().is_none()
&& let Some(neighbour) = self.objects.get(index)
&& let Some(stream) = neighbour.content_stream()
{
object.set_content_stream(Some(stream));
self.dirty_streams.insert(Some(stream));
}
self.objects.insert(index, object);
Ok(())
}
pub fn remove_object(&mut self, index: usize) -> Option<PageObject> {
if index >= self.objects.len() {
return None;
}
let object = self.objects.remove(index);
if let Some(stream) = object.content_stream() {
self.dirty_streams.insert(Some(stream));
}
Some(object)
}
#[must_use]
pub fn is_dirty(&self) -> bool {
!self.dirty_streams.is_empty() || self.objects.iter().any(PageObject::is_dirty)
}
#[must_use]
pub fn dirty_stream_set(&self) -> BTreeSet<Option<usize>> {
let mut set = self.dirty_streams.clone();
for object in &self.objects {
if object.is_dirty() {
set.insert(object.content_stream());
}
}
set
}
pub fn mark_clean(&mut self) {
self.dirty_streams.clear();
for object in &mut self.objects {
object.mark_clean();
}
}
#[must_use]
pub fn ctm_at_start_of_stream(&self, stream: Option<usize>) -> kurbo::Affine {
let Some(stream) = stream else {
return self
.stream_ctms
.values()
.next_back()
.copied()
.unwrap_or(kurbo::Affine::IDENTITY);
};
if stream == 0 || self.stream_ctms.is_empty() {
return kurbo::Affine::IDENTITY;
}
self.ctm_at_end_of_stream(stream.saturating_sub(1))
}
#[must_use]
pub fn ctm_at_end_of_stream(&self, stream: usize) -> kurbo::Affine {
self.stream_ctms
.range(stream..)
.next()
.or_else(|| self.stream_ctms.iter().next_back())
.map_or(kurbo::Affine::IDENTITY, |(_, m)| *m)
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::indexing_slicing,
reason = "test fixtures index arrays whose length the fixture fixes"
)]
use super::IndexOutOfRange;
use crate::page::{Content, Page, PageObject, PathObject};
use crate::state::{ContentMarks, GraphicsState};
use kurbo::{Affine, BezPath};
fn object(stream: Option<usize>) -> PageObject {
PageObject::Path(Box::new(Content {
object: PathObject {
path: BezPath::new(),
matrix: Affine::IDENTITY,
fill_rule: crate::ops::FillRule::Winding,
stroke: false,
},
state: GraphicsState::default(),
marks: ContentMarks::new(),
content_stream: stream,
dirty: false,
active: true,
}))
}
fn page(streams: &[Option<usize>]) -> Page {
Page {
objects: streams.iter().copied().map(object).collect(),
..Page::empty()
}
}
fn page_of(streams: &[usize]) -> Page {
page(&streams.iter().copied().map(Some).collect::<Vec<_>>())
}
#[test]
fn an_untouched_page_is_clean() {
let page = page_of(&[0, 0, 1]);
assert!(!page.is_dirty());
assert!(page.dirty_stream_set().is_empty());
}
#[test]
fn taking_a_mutable_object_dirties_it() {
let mut page = page_of(&[0, 1]);
assert!(page.object_mut(1).is_some());
assert!(page.is_dirty());
assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
let mut clean = page.clone();
clean.mark_clean();
let _ = clean.objects();
assert!(!clean.is_dirty());
}
#[test]
fn an_appended_object_is_dirty_and_streamless() {
let mut page = page_of(&[0]);
page.push_object(object(Some(7)));
let added = page.objects.last().expect("pushed");
assert!(added.is_dirty());
assert_eq!(added.content_stream(), None);
assert_eq!(page.dirty_stream_set(), [None].into_iter().collect());
}
#[test]
fn an_inserted_object_adopts_its_neighbours_stream() {
let mut page = page_of(&[0, 2, 2]);
let mut fresh = object(None);
fresh.set_content_stream(None);
assert!(page.insert_object(1, fresh).is_ok());
assert_eq!(page.objects.len(), 4);
assert_eq!(page.objects[1].content_stream(), Some(2));
assert!(page.dirty_streams.contains(&Some(2)));
}
#[test]
fn an_insert_past_the_end_is_refused() {
let mut page = page_of(&[0]);
let err = page
.insert_object(2, object(None))
.expect_err("past the end");
assert_eq!(err, IndexOutOfRange { index: 2, len: 1 });
assert_eq!(page.objects.len(), 1);
assert!(page.insert_object(1, object(None)).is_ok());
assert_eq!(page.objects.len(), 2);
}
#[test]
fn removing_an_object_records_its_stream() {
let mut page = page_of(&[0, 3]);
let removed = page.remove_object(1).expect("removed");
assert_eq!(removed.content_stream(), Some(3));
assert_eq!(page.objects.len(), 1);
assert!(page.is_dirty());
assert_eq!(page.dirty_stream_set(), [Some(3)].into_iter().collect());
}
#[test]
fn removing_a_streamless_object_records_nothing() {
let mut page = page(&[None]);
assert!(page.remove_object(0).is_some());
assert!(page.dirty_streams.is_empty());
assert!(!page.is_dirty());
assert!(page.remove_object(0).is_none());
}
#[test]
fn deactivating_an_object_dirties_its_stream() {
let mut page = page_of(&[0, 1]);
page.objects[1].set_active(false);
assert!(!page.objects[1].is_active());
assert!(page.objects[1].is_dirty());
assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
assert_eq!(page.active_objects().count(), 1);
}
#[test]
fn setting_activity_to_what_it_already_is_changes_nothing() {
let mut page = page_of(&[0]);
page.objects[0].set_active(true);
assert!(!page.is_dirty());
}
#[test]
fn a_save_leaves_the_page_clean() {
let mut page = page_of(&[0, 1]);
page.object_mut(0);
let _ = page.remove_object(1);
assert!(page.is_dirty());
page.mark_clean();
assert!(!page.is_dirty());
assert!(page.objects.iter().all(|o| !o.is_dirty()));
}
#[test]
fn stream_zero_and_an_empty_map_begin_at_the_identity() {
let page = page_of(&[0]);
assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
assert_eq!(page.ctm_at_start_of_stream(Some(3)), Affine::IDENTITY);
assert_eq!(page.ctm_at_end_of_stream(0), Affine::IDENTITY);
}
#[test]
fn a_later_stream_begins_where_the_previous_one_ended() {
let mut page = page_of(&[0, 1]);
let scaled = Affine::scale(2.0);
let moved = Affine::translate((5.0, 0.0));
page.stream_ctms.insert(0, scaled);
page.stream_ctms.insert(1, moved);
assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
assert_eq!(page.ctm_at_start_of_stream(Some(1)), scaled);
assert_eq!(page.ctm_at_end_of_stream(1), moved);
assert_eq!(page.ctm_at_start_of_stream(None), moved);
}
#[test]
fn a_stream_with_no_entry_reads_the_next_one_that_has_it() {
let mut page = page_of(&[0]);
let scaled = Affine::scale(3.0);
page.stream_ctms.insert(0, Affine::IDENTITY);
page.stream_ctms.insert(4, scaled);
assert_eq!(page.ctm_at_end_of_stream(2), scaled);
assert_eq!(page.ctm_at_end_of_stream(9), scaled);
}
}