1use std::collections::BTreeSet;
36
37use crate::page::{Page, PageObject};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
44#[error("page object index {index} is out of range (len {len})")]
45pub struct IndexOutOfRange {
46 pub index: usize,
48 pub len: usize,
50}
51
52impl PageObject {
53 #[must_use]
58 pub fn is_dirty(&self) -> bool {
59 self.common().dirty
60 }
61
62 #[must_use]
68 pub fn is_active(&self) -> bool {
69 self.common().active
70 }
71
72 #[must_use]
75 pub fn content_stream(&self) -> Option<usize> {
76 self.common().content_stream
77 }
78
79 pub fn mark_dirty(&mut self) {
81 *self.common_mut().dirty = true;
82 }
83
84 pub fn mark_clean(&mut self) {
87 *self.common_mut().dirty = false;
88 }
89
90 pub fn set_active(&mut self, active: bool) {
97 let common = self.common_mut();
98 if *common.active == active {
99 return;
100 }
101 *common.active = active;
102 *common.dirty = true;
103 }
104
105 pub fn set_content_stream(&mut self, stream: Option<usize>) {
110 *self.common_mut().content_stream = stream;
111 }
112}
113
114impl Page {
115 #[must_use]
117 pub fn objects(&self) -> &[PageObject] {
118 &self.objects
119 }
120
121 pub fn active_objects(&self) -> impl Iterator<Item = &PageObject> {
123 self.objects.iter().filter(|o| o.is_active())
124 }
125
126 pub fn object_mut(&mut self, index: usize) -> Option<&mut PageObject> {
132 let object = self.objects.get_mut(index)?;
133 object.mark_dirty();
134 Some(object)
135 }
136
137 pub fn push_object(&mut self, mut object: PageObject) {
143 object.mark_dirty();
144 object.set_content_stream(None);
145 self.objects.push(object);
146 }
147
148 pub fn insert_object(
159 &mut self,
160 index: usize,
161 mut object: PageObject,
162 ) -> Result<(), IndexOutOfRange> {
163 let len = self.objects.len();
164 if index > len {
165 return Err(IndexOutOfRange { index, len });
166 }
167 object.mark_dirty();
168 if object.content_stream().is_none()
169 && let Some(neighbour) = self.objects.get(index)
170 && let Some(stream) = neighbour.content_stream()
171 {
172 object.set_content_stream(Some(stream));
173 self.dirty_streams.insert(Some(stream));
174 }
175 self.objects.insert(index, object);
176 Ok(())
177 }
178
179 pub fn remove_object(&mut self, index: usize) -> Option<PageObject> {
184 if index >= self.objects.len() {
185 return None;
186 }
187 let object = self.objects.remove(index);
188 if let Some(stream) = object.content_stream() {
189 self.dirty_streams.insert(Some(stream));
190 }
191 Some(object)
192 }
193
194 #[must_use]
199 pub fn is_dirty(&self) -> bool {
200 !self.dirty_streams.is_empty() || self.objects.iter().any(PageObject::is_dirty)
201 }
202
203 #[must_use]
209 pub fn dirty_stream_set(&self) -> BTreeSet<Option<usize>> {
210 let mut set = self.dirty_streams.clone();
211 for object in &self.objects {
212 if object.is_dirty() {
213 set.insert(object.content_stream());
214 }
215 }
216 set
217 }
218
219 pub fn mark_clean(&mut self) {
223 self.dirty_streams.clear();
224 for object in &mut self.objects {
225 object.mark_clean();
226 }
227 }
228
229 #[must_use]
239 pub fn ctm_at_start_of_stream(&self, stream: Option<usize>) -> kurbo::Affine {
240 let Some(stream) = stream else {
241 return self
243 .stream_ctms
244 .values()
245 .next_back()
246 .copied()
247 .unwrap_or(kurbo::Affine::IDENTITY);
248 };
249 if stream == 0 || self.stream_ctms.is_empty() {
250 return kurbo::Affine::IDENTITY;
251 }
252 self.ctm_at_end_of_stream(stream.saturating_sub(1))
253 }
254
255 #[must_use]
261 pub fn ctm_at_end_of_stream(&self, stream: usize) -> kurbo::Affine {
262 self.stream_ctms
263 .range(stream..)
264 .next()
265 .or_else(|| self.stream_ctms.iter().next_back())
266 .map_or(kurbo::Affine::IDENTITY, |(_, m)| *m)
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 #![allow(
275 clippy::indexing_slicing,
276 reason = "test fixtures index arrays whose length the fixture fixes"
277 )]
278
279 use super::IndexOutOfRange;
280 use crate::page::{Content, Page, PageObject, PathObject};
281 use crate::state::{ContentMarks, GraphicsState};
282 use kurbo::{Affine, BezPath};
283
284 fn object(stream: Option<usize>) -> PageObject {
285 PageObject::Path(Box::new(Content {
286 object: PathObject {
287 path: BezPath::new(),
288 matrix: Affine::IDENTITY,
289 fill_rule: crate::ops::FillRule::Winding,
290 stroke: false,
291 },
292 state: GraphicsState::default(),
293 marks: ContentMarks::new(),
294 content_stream: stream,
295 dirty: false,
296 active: true,
297 }))
298 }
299
300 fn page(streams: &[Option<usize>]) -> Page {
301 Page {
302 objects: streams.iter().copied().map(object).collect(),
303 ..Page::empty()
304 }
305 }
306
307 fn page_of(streams: &[usize]) -> Page {
309 page(&streams.iter().copied().map(Some).collect::<Vec<_>>())
310 }
311
312 #[test]
314 fn an_untouched_page_is_clean() {
315 let page = page_of(&[0, 0, 1]);
316 assert!(!page.is_dirty());
317 assert!(page.dirty_stream_set().is_empty());
318 }
319
320 #[test]
321 fn taking_a_mutable_object_dirties_it() {
322 let mut page = page_of(&[0, 1]);
323 assert!(page.object_mut(1).is_some());
324 assert!(page.is_dirty());
325 assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
326 let mut clean = page.clone();
328 clean.mark_clean();
329 let _ = clean.objects();
330 assert!(!clean.is_dirty());
331 }
332
333 #[test]
334 fn an_appended_object_is_dirty_and_streamless() {
335 let mut page = page_of(&[0]);
336 page.push_object(object(Some(7)));
337 let added = page.objects.last().expect("pushed");
338 assert!(added.is_dirty());
339 assert_eq!(added.content_stream(), None);
340 assert_eq!(page.dirty_stream_set(), [None].into_iter().collect());
341 }
342
343 #[test]
346 fn an_inserted_object_adopts_its_neighbours_stream() {
347 let mut page = page_of(&[0, 2, 2]);
348 let mut fresh = object(None);
349 fresh.set_content_stream(None);
350 assert!(page.insert_object(1, fresh).is_ok());
351 assert_eq!(page.objects.len(), 4);
352 assert_eq!(page.objects[1].content_stream(), Some(2));
353 assert!(page.dirty_streams.contains(&Some(2)));
354 }
355
356 #[test]
357 fn an_insert_past_the_end_is_refused() {
358 let mut page = page_of(&[0]);
359 let err = page
360 .insert_object(2, object(None))
361 .expect_err("past the end");
362 assert_eq!(err, IndexOutOfRange { index: 2, len: 1 });
363 assert_eq!(page.objects.len(), 1);
364 assert!(page.insert_object(1, object(None)).is_ok());
366 assert_eq!(page.objects.len(), 2);
367 }
368
369 #[test]
372 fn removing_an_object_records_its_stream() {
373 let mut page = page_of(&[0, 3]);
374 let removed = page.remove_object(1).expect("removed");
375 assert_eq!(removed.content_stream(), Some(3));
376 assert_eq!(page.objects.len(), 1);
377 assert!(page.is_dirty());
378 assert_eq!(page.dirty_stream_set(), [Some(3)].into_iter().collect());
379 }
380
381 #[test]
382 fn removing_a_streamless_object_records_nothing() {
383 let mut page = page(&[None]);
384 assert!(page.remove_object(0).is_some());
385 assert!(page.dirty_streams.is_empty());
386 assert!(!page.is_dirty());
387 assert!(page.remove_object(0).is_none());
388 }
389
390 #[test]
393 fn deactivating_an_object_dirties_its_stream() {
394 let mut page = page_of(&[0, 1]);
395 page.objects[1].set_active(false);
396 assert!(!page.objects[1].is_active());
397 assert!(page.objects[1].is_dirty());
398 assert_eq!(page.dirty_stream_set(), [Some(1)].into_iter().collect());
399 assert_eq!(page.active_objects().count(), 1);
400 }
401
402 #[test]
403 fn setting_activity_to_what_it_already_is_changes_nothing() {
404 let mut page = page_of(&[0]);
405 page.objects[0].set_active(true);
406 assert!(!page.is_dirty());
407 }
408
409 #[test]
410 fn a_save_leaves_the_page_clean() {
411 let mut page = page_of(&[0, 1]);
412 page.object_mut(0);
413 let _ = page.remove_object(1);
414 assert!(page.is_dirty());
415 page.mark_clean();
416 assert!(!page.is_dirty());
417 assert!(page.objects.iter().all(|o| !o.is_dirty()));
418 }
419
420 #[test]
422 fn stream_zero_and_an_empty_map_begin_at_the_identity() {
423 let page = page_of(&[0]);
424 assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
425 assert_eq!(page.ctm_at_start_of_stream(Some(3)), Affine::IDENTITY);
426 assert_eq!(page.ctm_at_end_of_stream(0), Affine::IDENTITY);
427 }
428
429 #[test]
430 fn a_later_stream_begins_where_the_previous_one_ended() {
431 let mut page = page_of(&[0, 1]);
432 let scaled = Affine::scale(2.0);
433 let moved = Affine::translate((5.0, 0.0));
434 page.stream_ctms.insert(0, scaled);
435 page.stream_ctms.insert(1, moved);
436 assert_eq!(page.ctm_at_start_of_stream(Some(0)), Affine::IDENTITY);
437 assert_eq!(page.ctm_at_start_of_stream(Some(1)), scaled);
438 assert_eq!(page.ctm_at_end_of_stream(1), moved);
439 assert_eq!(page.ctm_at_start_of_stream(None), moved);
442 }
443
444 #[test]
447 fn a_stream_with_no_entry_reads_the_next_one_that_has_it() {
448 let mut page = page_of(&[0]);
449 let scaled = Affine::scale(3.0);
450 page.stream_ctms.insert(0, Affine::IDENTITY);
451 page.stream_ctms.insert(4, scaled);
452 assert_eq!(page.ctm_at_end_of_stream(2), scaled);
453 assert_eq!(page.ctm_at_end_of_stream(9), scaled);
455 }
456}