| ... | ... | @@ -624,6 +624,11 @@ enum History { |
| 624 | 624 | image: ExGuid, |
| 625 | 625 | layout: onestore::document::Layout, |
| 626 | 626 | }, |
| 627 | /// Restores the picture at `index` in paint order, or removes the one there when `None`. |
| 628 | Picture { |
| 629 | index: usize, |
| 630 | image: Option<Box<onestore::page::Image>>, |
| 631 | }, |
| 627 | 632 | Remove { |
| 628 | 633 | outline: onestore::ExGuid, |
| 629 | 634 | focus: RestoreFocus, |
| ... | ... | @@ -1336,6 +1341,62 @@ impl CanvasEditor { |
| 1336 | 1341 | Ok(()) |
| 1337 | 1342 | } |
| 1338 | 1343 | |
| 1344 | /// Leaves a picture as OneNote's Left and Right arrows do: the caret goes to the end of the |
| 1345 | /// text outline before it in page order, or to the start of the one after. False when there |
| 1346 | /// is none. |
| 1347 | pub fn step_from_image( |
| 1348 | &mut self, |
| 1349 | engine: &mut TextEngine, |
| 1350 | id: ExGuid, |
| 1351 | forward: bool, |
| 1352 | ) -> Result<bool, EditorError> { |
| 1353 | let index = self |
| 1354 | .objects |
| 1355 | .iter() |
| 1356 | .position(|object| matches!(object, page::Content::Image(image) if image.id == id)) |
| 1357 | .ok_or(EditError::InvalidRange)?; |
| 1358 | let editable = |object: &page::Content| match object { |
| 1359 | page::Content::Editable(outline) => Some(*outline), |
| 1360 | _ => None, |
| 1361 | }; |
| 1362 | let outline = if forward { |
| 1363 | self.objects[index + 1..].iter().find_map(editable) |
| 1364 | } else { |
| 1365 | self.objects[..index].iter().rev().find_map(editable) |
| 1366 | }; |
| 1367 | let Some(outline) = outline else { |
| 1368 | return Ok(false); |
| 1369 | }; |
| 1370 | self.focus_outline(outline)?; |
| 1371 | let edge = if forward { |
| 1372 | Movement::DocumentStart |
| 1373 | } else { |
| 1374 | Movement::DocumentEnd |
| 1375 | }; |
| 1376 | self.move_selection(engine, edge, false)?; |
| 1377 | Ok(true) |
| 1378 | } |
| 1379 | |
| 1380 | pub fn remove_image(&mut self, id: ExGuid) -> Result<(), EditError> { |
| 1381 | let index = self |
| 1382 | .objects |
| 1383 | .iter() |
| 1384 | .position(|object| { |
| 1385 | matches!(object, page::Content::Image(image) if image.id == id && !image.background) |
| 1386 | }) |
| 1387 | .ok_or(EditError::InvalidRange)?; |
| 1388 | self.finish_composition(); |
| 1389 | let page::Content::Image(image) = self.objects.remove(index) else { |
| 1390 | unreachable!() |
| 1391 | }; |
| 1392 | self.undo.push(History::Picture { |
| 1393 | index, |
| 1394 | image: Some(Box::new(image)), |
| 1395 | }); |
| 1396 | self.redo.clear(); |
| 1397 | Ok(()) |
| 1398 | } |
| 1399 | |
| 1339 | 1400 | pub fn selection(&self) -> Selection { |
| 1340 | 1401 | self.active_outline().selection |
| 1341 | 1402 | } |
| ... | ... | @@ -2315,6 +2376,13 @@ impl CanvasEditor { |
| 2315 | 2376 | self.outlines.iter().any(|item| item.id == *outline) |
| 2316 | 2377 | } |
| 2317 | 2378 | History::Image { image, .. } => self.image(*image).is_some(), |
| 2379 | History::Picture { |
| 2380 | index, |
| 2381 | image: Some(_), |
| 2382 | } => *index <= self.objects.len(), |
| 2383 | History::Picture { index, image: None } => { |
| 2384 | matches!(self.objects.get(*index), Some(page::Content::Image(_))) |
| 2385 | } |
| 2318 | 2386 | History::Remove { outline, focus } => { |
| 2319 | 2387 | self.outlines.iter().any(|item| item.id == *outline) |
| 2320 | 2388 | && match focus { |
| ... | ... | @@ -2466,6 +2534,19 @@ impl CanvasEditor { |
| 2466 | 2534 | image, |
| 2467 | 2535 | layout: std::mem::replace(&mut self.image_mut(image).unwrap().layout, layout), |
| 2468 | 2536 | }, |
| 2537 | History::Picture { index, image } => History::Picture { |
| 2538 | index, |
| 2539 | image: match image { |
| 2540 | Some(image) => { |
| 2541 | self.objects.insert(index, page::Content::Image(*image)); |
| 2542 | None |
| 2543 | } |
| 2544 | None => match self.objects.remove(index) { |
| 2545 | page::Content::Image(image) => Some(Box::new(image)), |
| 2546 | _ => unreachable!(), |
| 2547 | }, |
| 2548 | }, |
| 2549 | }, |
| 2469 | 2550 | History::Remove { outline, focus } => { |
| 2470 | 2551 | let index = self |
| 2471 | 2552 | .outlines |
| ... | ... | @@ -4479,6 +4560,88 @@ mod tests { |
| 4479 | 4560 | assert!(editor.redo(&mut engine).unwrap()); |
| 4480 | 4561 | assert!(editor.redo(&mut engine).unwrap()); |
| 4481 | 4562 | assert_eq!(stored(&editor), resized); |
| 4563 | |
| 4564 | let ids = |editor: &CanvasEditor| { |
| 4565 | editor |
| 4566 | .page() |
| 4567 | .unwrap() |
| 4568 | .objects |
| 4569 | .iter() |
| 4570 | .map(PageObject::id) |
| 4571 | .collect::<Vec<_>>() |
| 4572 | }; |
| 4573 | let before = ids(&editor); |
| 4574 | assert_eq!( |
| 4575 | editor.remove_image(background_id), |
| 4576 | Err(EditError::InvalidRange) |
| 4577 | ); |
| 4578 | editor.remove_image(id).unwrap(); |
| 4579 | assert!(editor.image_placement(id).is_none()); |
| 4580 | assert_eq!(ids(&editor), [background_id]); |
| 4581 | assert!(editor.undo(&mut engine).unwrap()); |
| 4582 | assert_eq!(ids(&editor), before); |
| 4583 | assert_eq!(stored(&editor), resized); |
| 4584 | assert!(editor.redo(&mut engine).unwrap()); |
| 4585 | assert_eq!(ids(&editor), [background_id]); |
| 4586 | } |
| 4587 | |
| 4588 | #[test] |
| 4589 | fn arrows_leave_a_picture_for_the_neighbouring_outlines_in_page_order() { |
| 4590 | use onestore::page::{Image, Page, PageObject}; |
| 4591 | let mut engine = TextEngine::default(); |
| 4592 | let outline = |engine: &mut TextEngine, text: &str, x| { |
| 4593 | PageObject::Outline( |
| 4594 | TextOutline::new( |
| 4595 | engine, |
| 4596 | TextDocument::new(vec![Paragraph::new(text.into(), Default::default())]) |
| 4597 | .unwrap(), |
| 4598 | 120.0, |
| 4599 | [x, 0.0], |
| 4600 | ) |
| 4601 | .unwrap() |
| 4602 | .snapshot(), |
| 4603 | ) |
| 4604 | }; |
| 4605 | let picture = Image { |
| 4606 | size: None, |
| 4607 | id: onestore::page::text::new_id().unwrap(), |
| 4608 | layout: onestore::document::Layout { |
| 4609 | x: Some(200.0), |
| 4610 | y: Some(0.0), |
| 4611 | max_width: Some(40.0), |
| 4612 | max_height: Some(40.0), |
| 4613 | ..Default::default() |
| 4614 | }, |
| 4615 | bytes: Some(std::sync::Arc::from(b"deferred image payload".as_slice())), |
| 4616 | alt: None, |
| 4617 | background: false, |
| 4618 | }; |
| 4619 | let id = picture.id; |
| 4620 | let objects = vec![ |
| 4621 | outline(&mut engine, "Before", 0.0), |
| 4622 | PageObject::Image(picture), |
| 4623 | outline(&mut engine, "After", 300.0), |
| 4624 | ]; |
| 4625 | let [before, after] = [0, 2].map(|index| objects[index].id()); |
| 4626 | let mut editor = CanvasEditor::from_page( |
| 4627 | Page { |
| 4628 | title: String::new(), |
| 4629 | identity: None, |
| 4630 | created: None, |
| 4631 | margin_origin: [36.0, 14.4], |
| 4632 | definitions: BTreeMap::new(), |
| 4633 | objects, |
| 4634 | }, |
| 4635 | &mut engine, |
| 4636 | ) |
| 4637 | .unwrap(); |
| 4638 | let caret = |paragraph, offset| [TextPosition { paragraph, offset }; 2]; |
| 4639 | assert!(editor.step_from_image(&mut engine, id, false).unwrap()); |
| 4640 | assert_eq!(editor.active_outline().id, before); |
| 4641 | assert_eq!(editor.selection().positions, caret(0, 6)); |
| 4642 | assert!(editor.step_from_image(&mut engine, id, true).unwrap()); |
| 4643 | assert_eq!(editor.active_outline().id, after); |
| 4644 | assert_eq!(editor.selection().positions, caret(0, 0)); |
| 4482 | 4645 | } |
| 4483 | 4646 | |
| 4484 | 4647 | #[test] |