authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-24 11:19:41-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:21-07:00
log7b577f2383c4ed4a5d5a84a8b564d088198a773e
tree992e2d4188c36394bdc46735cbfaad4c1893f2e8
parentb822df2242bfd846f6475e49af173e4736187b4b
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: select, resize and delete pictures inside outlines

A picture in an outline's flow took no clicks. It now selects like a page picture: its handles resize it and Delete removes it, both through the outline's text history so undo restores the layout around it, while a drag leaves it in place. Left and Right put the caret in the text beside it. Assisted-by: claude-opus-5.5

4 files changed, 276 insertions(+), 35 deletions(-)

crates/canvas/src/editor.rs+176-25
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1use crate::{1use crate::{
2 date::PageDate,2 date::PageDate,
3 document::{DocumentEdit, TextDocument, TextPosition, edited_nodes, leaves},3 document::{DocumentEdit, TextDocument, TextPosition, descendants, edited_nodes, leaves},
4 layout::{LayoutError, TextEngine},4 layout::{LayoutError, TextEngine},
5 outline::{5 outline::{
6 Arrange, OutlineLayout, ParagraphLayout, arrange, indentation, outline_layout,6 Arrange, OutlineLayout, ParagraphLayout, arrange, indentation, outline_layout,
...@@ -1278,11 +1278,60 @@ impl CanvasEditor {...@@ -1278,11 +1278,60 @@ impl CanvasEditor {
1278 /// Origin and size of a picture the user can select; OneNote passes clicks through1278 /// Origin and size of a picture the user can select; OneNote passes clicks through
1279 /// backgrounds.1279 /// backgrounds.
1280 pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> {1280 pub fn image_placement(&self, id: ExGuid) -> Option<([f32; 2], [f32; 2])> {
1281 let layout = &self.image(id)?.layout;1281 if let Some(image) = self.image(id) {
1282 Some((1282 let layout = &image.layout;
1283 [layout.x.unwrap_or(0.0), layout.y.unwrap_or(0.0)],1283 return Some((
1284 [layout.max_width?, layout.max_height?],1284 [layout.x.unwrap_or(0.0), layout.y.unwrap_or(0.0)],
1285 ))1285 [layout.max_width?, layout.max_height?],
1286 ));
1287 }
1288 let (outline, ..) = self.outline_picture(id)?;
1289 let outline = self.outlines.iter().find(|item| item.id == outline)?;
1290 let [x0, y0, x1, y1] = outline.shaped.objects.iter().find(|o| o.id == id)?.rect;
1291 let origin = outline.origin();
1292 Some(([origin[0] + x0, origin[1] + y0], [x1 - x0, y1 - y0]))
1293 }
1294
1295 /// Whether the picture sits in an outline's flow, where only its size can change.
1296 pub fn image_in_outline(&self, id: ExGuid) -> bool {
1297 self.outline_picture(id).is_some()
1298 }
1299
1300 /// A picture held as a paragraph: its outline, the paragraph's container and index there,
1301 /// and the paragraph.
1302 fn outline_picture(
1303 &self,
1304 id: ExGuid,
1305 ) -> Option<(ExGuid, Option<ExGuid>, usize, &PageParagraph)> {
1306 self.outlines.iter().find_map(|outline| {
1307 descendants(outline.document.nodes(), None).find_map(|(container, index, node)| {
1308 matches!(&node.content, onestore::page::ParagraphContent::Image(image) if image.id == id)
1309 .then_some((outline.id, container, index, node))
1310 })
1311 })
1312 }
1313
1314 /// Replaces a picture paragraph through the outline's text history.
1315 fn edit_outline_picture(
1316 &mut self,
1317 engine: &mut TextEngine,
1318 id: ExGuid,
1319 replacement: Option<PageParagraph>,
1320 ) -> Result<(), EditorError> {
1321 let (outline, container, index, _) =
1322 self.outline_picture(id).ok_or(EditError::InvalidRange)?;
1323 self.focus_outline(outline)?;
1324 let selection = self.selection();
1325 self.commit(
1326 engine,
1327 DocumentEdit {
1328 columns: BTreeMap::new(),
1329 container,
1330 range: index..index + 1,
1331 replacement: replacement.into_iter().collect(),
1332 },
1333 selection,
1334 )
1286 }1335 }
12871336
1288 fn image(&self, id: ExGuid) -> Option<&onestore::page::Image> {1337 fn image(&self, id: ExGuid) -> Option<&onestore::page::Image> {
...@@ -1299,17 +1348,31 @@ impl CanvasEditor {...@@ -1299,17 +1348,31 @@ impl CanvasEditor {
1299 })1348 })
1300 }1349 }
13011350
1302 /// Moves a picture, and resizes it when `size` differs from the stored size.1351 /// Moves a picture, and resizes it when `size` differs from the stored size. A picture in
1352 /// an outline's flow keeps its place.
1303 pub fn place_image(1353 pub fn place_image(
1304 &mut self,1354 &mut self,
1355 engine: &mut TextEngine,
1305 id: ExGuid,1356 id: ExGuid,
1306 origin: [f32; 2],1357 origin: [f32; 2],
1307 size: [f32; 2],1358 size: [f32; 2],
1308 ) -> Result<(), EditError> {1359 ) -> Result<(), EditorError> {
1309 if !origin.iter().chain(&size).all(|value| value.is_finite())1360 if !origin.iter().chain(&size).all(|value| value.is_finite())
1310 || size.iter().any(|value| *value <= 0.0)1361 || size.iter().any(|value| *value <= 0.0)
1311 {1362 {
1312 return Err(EditError::InvalidRange);1363 return Err(EditError::InvalidRange.into());
1364 }
1365 if let Some((.., node)) = self.outline_picture(id) {
1366 let mut node = node.clone();
1367 let onestore::page::ParagraphContent::Image(image) = &mut node.content else {
1368 unreachable!()
1369 };
1370 if crate::outline::image_size(image) == Some(size) {
1371 return Ok(());
1372 }
1373 [image.layout.max_width, image.layout.max_height] = size.map(Some);
1374 image.layout.width_set_by_user = Some(true);
1375 return self.edit_outline_picture(engine, id, Some(node));
1313 }1376 }
1314 let image = self.image(id).ok_or(EditError::InvalidRange)?;1377 let image = self.image(id).ok_or(EditError::InvalidRange)?;
1315 let mut layout = image.layout.clone();1378 let mut layout = image.layout.clone();
...@@ -1340,6 +1403,44 @@ impl CanvasEditor {...@@ -1340,6 +1403,44 @@ impl CanvasEditor {
1340 id: ExGuid,1403 id: ExGuid,
1341 forward: bool,1404 forward: bool,
1342 ) -> Result<bool, EditorError> {1405 ) -> Result<bool, EditorError> {
1406 if let Some((outline, ..)) = self.outline_picture(id) {
1407 // Within an outline, the caret goes to the text beside the picture.
1408 let source = self
1409 .outlines
1410 .iter()
1411 .find(|item| item.id == outline)
1412 .unwrap();
1413 let mut leaf = 0;
1414 let mut before = None;
1415 let mut after = None;
1416 let mut passed = false;
1417 for (_, _, node) in descendants(source.document.nodes(), None) {
1418 match &node.content {
1419 onestore::page::ParagraphContent::Image(image) if image.id == id => {
1420 passed = true
1421 }
1422 onestore::page::ParagraphContent::Text(text) => {
1423 let end = text.text.utf16_offset(text.text.text().len())?;
1424 if passed {
1425 after = after.or(Some((leaf, 0)));
1426 } else {
1427 before = Some((leaf, end));
1428 }
1429 leaf += 1;
1430 }
1431 _ => {}
1432 }
1433 }
1434 let Some((paragraph, offset)) = (if forward { after } else { before }) else {
1435 return Ok(false);
1436 };
1437 self.focus_outline(outline)?;
1438 self.select(Selection {
1439 positions: [TextPosition { paragraph, offset }; 2],
1440 affinities: [Affinity::Downstream; 2],
1441 })?;
1442 return Ok(true);
1443 }
1343 let index = self1444 let index = self
1344 .objects1445 .objects
1345 .iter()1446 .iter()
...@@ -1367,7 +1468,10 @@ impl CanvasEditor {...@@ -1367,7 +1468,10 @@ impl CanvasEditor {
1367 Ok(true)1468 Ok(true)
1368 }1469 }
13691470
1370 pub fn remove_image(&mut self, id: ExGuid) -> Result<(), EditError> {1471 pub fn remove_image(&mut self, engine: &mut TextEngine, id: ExGuid) -> Result<(), EditorError> {
1472 if self.outline_picture(id).is_some() {
1473 return self.edit_outline_picture(engine, id, None);
1474 }
1371 let index = self1475 let index = self
1372 .objects1476 .objects
1373 .iter()1477 .iter()
...@@ -4538,22 +4642,22 @@ mod tests {...@@ -4538,22 +4642,22 @@ mod tests {
4538 Some(([468.75, 86.4], [333.0, 200.1]))4642 Some(([468.75, 86.4], [333.0, 200.1]))
4539 );4643 );
4540 assert!(editor.image_placement(background_id).is_none());4644 assert!(editor.image_placement(background_id).is_none());
4541 assert_eq!(4645 assert!(matches!(
4542 editor.place_image(background_id, [0.0; 2], [10.0; 2]),4646 editor.place_image(&mut engine, background_id, [0.0; 2], [10.0; 2]),
4543 Err(EditError::InvalidRange)4647 Err(EditorError::Edit(EditError::InvalidRange))
4544 );4648 ));
4545 assert_eq!(4649 assert!(matches!(
4546 editor.place_image(id, [0.0; 2], [0.0, 10.0]),4650 editor.place_image(&mut engine, id, [0.0; 2], [0.0, 10.0]),
4547 Err(EditError::InvalidRange)4651 Err(EditorError::Edit(EditError::InvalidRange))
4548 );4652 ));
4549 editor4653 editor
4550 .place_image(id, [513.75, 104.4], [333.0, 200.1])4654 .place_image(&mut engine, id, [513.75, 104.4], [333.0, 200.1])
4551 .unwrap();4655 .unwrap();
4552 let moved = stored(&editor);4656 let moved = stored(&editor);
4553 assert_eq!([moved.x, moved.y], [Some(513.75), Some(104.4)]);4657 assert_eq!([moved.x, moved.y], [Some(513.75), Some(104.4)]);
4554 assert_eq!(moved.width_set_by_user, None);4658 assert_eq!(moved.width_set_by_user, None);
4555 editor4659 editor
4556 .place_image(id, [513.75, 104.4], [281.8, 169.4])4660 .place_image(&mut engine, id, [513.75, 104.4], [281.8, 169.4])
4557 .unwrap();4661 .unwrap();
4558 let resized = stored(&editor);4662 let resized = stored(&editor);
4559 assert_eq!(4663 assert_eq!(
...@@ -4579,11 +4683,11 @@ mod tests {...@@ -4579,11 +4683,11 @@ mod tests {
4579 .collect::<Vec<_>>()4683 .collect::<Vec<_>>()
4580 };4684 };
4581 let before = ids(&editor);4685 let before = ids(&editor);
4582 assert_eq!(4686 assert!(matches!(
4583 editor.remove_image(background_id),4687 editor.remove_image(&mut engine, background_id),
4584 Err(EditError::InvalidRange)4688 Err(EditorError::Edit(EditError::InvalidRange))
4585 );4689 ));
4586 editor.remove_image(id).unwrap();4690 editor.remove_image(&mut engine, id).unwrap();
4587 assert!(editor.image_placement(id).is_none());4691 assert!(editor.image_placement(id).is_none());
4588 assert_eq!(ids(&editor), [background_id]);4692 assert_eq!(ids(&editor), [background_id]);
4589 assert!(editor.undo(&mut engine).unwrap());4693 assert!(editor.undo(&mut engine).unwrap());
...@@ -4732,6 +4836,53 @@ mod tests {...@@ -4732,6 +4836,53 @@ mod tests {
4732 assert!(editor.undo(&mut engine).unwrap());4836 assert!(editor.undo(&mut engine).unwrap());
4733 assert_eq!(editor.active_outline().shaped().paragraphs.len(), 2);4837 assert_eq!(editor.active_outline().shaped().paragraphs.len(), 2);
4734 assert_eq!(editor.active_outline().shaped().objects.len(), 1);4838 assert_eq!(editor.active_outline().shaped().objects.len(), 1);
4839
4840 // Selected, it resizes and deletes in place through the outline's history.
4841 assert!(editor.image_in_outline(image.id));
4842 let (origin, size) = editor.image_placement(image.id).unwrap();
4843 assert_eq!(size, [40.0, 30.0]);
4844 editor
4845 .place_image(&mut engine, image.id, [0.0; 2], [80.0, 60.0])
4846 .unwrap();
4847 assert_eq!(
4848 editor.image_placement(image.id),
4849 Some((origin, [80.0, 60.0]))
4850 );
4851 let shaped = editor.active_outline().shaped();
4852 assert!(shaped.paragraphs[1].origin[1] >= shaped.objects[0].bottom);
4853 assert!(editor.step_from_image(&mut engine, image.id, true).unwrap());
4854 assert_eq!(
4855 editor.selection().positions[0],
4856 TextPosition {
4857 paragraph: 1,
4858 offset: 0
4859 }
4860 );
4861 assert!(
4862 editor
4863 .step_from_image(&mut engine, image.id, false)
4864 .unwrap()
4865 );
4866 assert_eq!(
4867 editor.selection().positions[0],
4868 TextPosition {
4869 paragraph: 0,
4870 offset: 6
4871 }
4872 );
4873 editor.remove_image(&mut engine, image.id).unwrap();
4874 assert!(editor.image_placement(image.id).is_none());
4875 assert!(editor.active_outline().shaped().objects.is_empty());
4876 assert!(editor.undo(&mut engine).unwrap());
4877 assert_eq!(
4878 editor.image_placement(image.id),
4879 Some((origin, [80.0, 60.0]))
4880 );
4881 assert!(editor.undo(&mut engine).unwrap());
4882 assert_eq!(
4883 editor.image_placement(image.id),
4884 Some((origin, [40.0, 30.0]))
4885 );
4735 }4886 }
47364887
4737 #[test]4888 #[test]
crates/canvas/src/gpu/page.rs+12-7
...@@ -213,20 +213,25 @@ impl PageScene {...@@ -213,20 +213,25 @@ impl PageScene {
213 }213 }
214214
215 /// Pictures, files and handwriting inside an outline whose origin is `origin`.215 /// Pictures, files and handwriting inside an outline whose origin is `origin`.
216 /// `moving` draws one picture at a previewed rectangle, in the same coordinates as `origin`.
216 pub fn append_outline_objects<'a>(217 pub fn append_outline_objects<'a>(
217 &'a self,218 &'a self,
218 outline: &'a crate::outline::OutlineLayout,219 outline: &'a crate::outline::OutlineLayout,
219 origin: [f32; 2],220 origin: [f32; 2],
221 moving: Option<(onestore::ExGuid, [f32; 4])>,
220 primitives: &mut Vec<Primitive<'a>>,222 primitives: &mut Vec<Primitive<'a>>,
221 ) {223 ) {
222 for object in &outline.objects {224 for object in &outline.objects {
223 let [x0, y0, x1, y1] = object.rect;225 let [x0, y0, x1, y1] = object.rect;
224 let rect = [226 let rect = match moving {
225 x0 + origin[0],227 Some((id, rect)) if id == object.id => rect,
226 y0 + origin[1],228 _ => [
227 x1 + origin[0],229 x0 + origin[0],
228 y1 + origin[1],230 y0 + origin[1],
229 ];231 x1 + origin[0],
232 y1 + origin[1],
233 ],
234 };
230 if let Some(image) = self.images.get(&object.id) {235 if let Some(image) = self.images.get(&object.id) {
231 primitives.push(Primitive::Image { image, rect });236 primitives.push(Primitive::Image { image, rect });
232 }237 }
...@@ -551,7 +556,7 @@ impl PageScene {...@@ -551,7 +556,7 @@ impl PageScene {
551 };556 };
552 outline.append_table_primitives(primitives, object_origin);557 outline.append_table_primitives(primitives, object_origin);
553 outline.append_background_primitives(primitives, object_origin);558 outline.append_background_primitives(primitives, object_origin);
554 self.append_outline_objects(outline, object_origin, primitives);559 self.append_outline_objects(outline, object_origin, None, primitives);
555 for (index, paragraph) in outline.paragraphs.iter().enumerate() {560 for (index, paragraph) in outline.paragraphs.iter().enumerate() {
556 outline.append_paragraph_primitives(561 outline.append_paragraph_primitives(
557 index,562 index,
crates/snowbound/src/main.rs+87-2
...@@ -394,6 +394,10 @@ impl State {...@@ -394,6 +394,10 @@ impl State {
394 let (origin, size) = resize_image(origin, size, *handle, delta);394 let (origin, size) = resize_image(origin, size, *handle, delta);
395 return Some((*id, origin, size));395 return Some((*id, origin, size));
396 }396 }
397 // A picture in an outline's flow keeps its place.
398 if self.editor.image_in_outline(*id) {
399 return Some((*id, origin, size));
400 }
397 let origin = [origin[0] + delta[0], origin[1] + delta[1]];401 let origin = [origin[0] + delta[0], origin[1] + delta[1]];
398 let origin = if self.modifiers.alt_key() {402 let origin = if self.modifiers.alt_key() {
399 origin403 origin
...@@ -892,7 +896,7 @@ impl State {...@@ -892,7 +896,7 @@ impl State {
892 if let Some(ObjectFocus::Image(id)) = self.object_focus896 if let Some(ObjectFocus::Image(id)) = self.object_focus
893 && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete))897 && matches!(key, Key::Named(NamedKey::Backspace | NamedKey::Delete))
894 {898 {
895 self.editor.remove_image(id)?;899 self.editor.remove_image(&mut self.engine, id)?;
896 self.set_object_focus(None);900 self.set_object_focus(None);
897 return self.changed();901 return self.changed();
898 }902 }
...@@ -1592,7 +1596,9 @@ impl ApplicationHandler<UserEvent> for App {...@@ -1592,7 +1596,9 @@ impl ApplicationHandler<UserEvent> for App {
1592 } else {1596 } else {
1593 let preview = state.preview();1597 let preview = state.preview();
1594 if let Some((id, origin, size)) = state.image_preview() {1598 if let Some((id, origin, size)) = state.image_preview() {
1595 state.editor.place_image(id, origin, size)?;1599 state
1600 .editor
1601 .place_image(&mut state.engine, id, origin, size)?;
1596 }1602 }
1597 if let Some(Drag::Resize {1603 if let Some(Drag::Resize {
1598 outline: Some(outline),1604 outline: Some(outline),
...@@ -1777,6 +1783,17 @@ fn page_hit_test(...@@ -1777,6 +1783,17 @@ fn page_hit_test(
1777 return None;1783 return None;
1778 }1784 }
1779 }1785 }
1786 let inner = [x - outline.origin()[0], y - outline.origin()[1]];
1787 if let Some(picture) = outline.shaped().objects.iter().find(|object| {
1788 matches!(object.kind, canvas::outline::ObjectKind::Picture)
1789 && (object.rect[0]..=object.rect[2]).contains(&inner[0])
1790 && (object.rect[1]..=object.rect[3]).contains(&inner[1])
1791 }) {
1792 return Some(Hit::Image {
1793 id: picture.id,
1794 handle: [0, 0],
1795 });
1796 }
1780 if (x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3])1797 if (x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3])
1781 || outline.layouts().any(|(_, paragraph)| {1798 || outline.layouts().any(|(_, paragraph)| {
1782 paragraph.tags.iter().any(|tag| {1799 paragraph.tags.iter().any(|tag| {
...@@ -1876,9 +1893,25 @@ fn page_primitives<'a>(...@@ -1876,9 +1893,25 @@ fn page_primitives<'a>(
1876 primitives,1893 primitives,
1877 )?;1894 )?;
1878 if let Some((scene, _)) = scene {1895 if let Some((scene, _)) = scene {
1896 let moving = match preview {
1897 Some(PointerFeedback::Image(id, origin, size)) => {
1898 let [x0, y0, x1, y1] = image_rect(origin, size);
1899 Some((
1900 id,
1901 [
1902 x0 + offset[0],
1903 y0 + offset[1],
1904 x1 + offset[0],
1905 y1 + offset[1],
1906 ],
1907 ))
1908 }
1909 _ => None,
1910 };
1879 scene.append_outline_objects(1911 scene.append_outline_objects(
1880 outline.shaped(),1912 outline.shaped(),
1881 [origin[0] + offset[0], origin[1] + offset[1]],1913 [origin[0] + offset[0], origin[1] + offset[1]],
1914 moving,
1882 primitives,1915 primitives,
1883 );1916 );
1884 }1917 }
...@@ -2748,6 +2781,58 @@ mod tests {...@@ -2748,6 +2781,58 @@ mod tests {
2748 );2781 );
2749 }2782 }
27502783
2784 #[test]
2785 fn a_picture_in_an_outline_takes_the_click_over_its_text() {
2786 use onestore::page::{Image, Page, PageObject, ParagraphContent};
2787 let mut engine = TextEngine::default();
2788 let mut source = TextOutline::new(
2789 &mut engine,
2790 TextDocument::new(vec![
2791 Paragraph::new("Before".into(), Default::default()),
2792 Paragraph::new("After".into(), Default::default()),
2793 ])
2794 .unwrap(),
2795 240.0,
2796 [36.0, 36.0],
2797 )
2798 .unwrap()
2799 .snapshot();
2800 let mut picture = source.paragraphs[0].clone();
2801 picture.id = onestore::page::text::new_id().unwrap();
2802 let id = onestore::page::text::new_id().unwrap();
2803 picture.content = ParagraphContent::Image(Image {
2804 size: Some([40.0, 30.0]),
2805 id,
2806 layout: Default::default(),
2807 bytes: None,
2808 alt: None,
2809 background: false,
2810 });
2811 source.paragraphs.insert(1, picture);
2812 let editor = CanvasEditor::from_page(
2813 Page {
2814 title: String::new(),
2815 identity: None,
2816 created: None,
2817 margin_origin: [36.0, 14.4],
2818 definitions: Default::default(),
2819 objects: vec![PageObject::Outline(source)],
2820 },
2821 &mut engine,
2822 )
2823 .unwrap();
2824 let (origin, size) = editor.image_placement(id).unwrap();
2825 let center = [origin[0] + size[0] / 2.0, origin[1] + size[1] / 2.0];
2826 assert_eq!(
2827 page_hit_test(&editor, None, center, 1.0),
2828 Some(Hit::Image { id, handle: [0, 0] })
2829 );
2830 assert!(matches!(
2831 page_hit_test(&editor, None, [origin[0] + 2.0, origin[1] - 4.0], 1.0),
2832 Some(Hit::Text { .. })
2833 ));
2834 }
2835
2751 #[test]2836 #[test]
2752 fn picture_handles_resize_as_onenote_does() {2837 fn picture_handles_resize_as_onenote_does() {
2753 // Native drags of the 333 x 200.1 pt mockup on "av: casual night in the trees".2838 // Native drags of the 333 x 200.1 pt mockup on "av: casual night in the trees".
tools/canvas/README.md+1-1
...@@ -131,7 +131,7 @@ CANVAS_TEST_SECTION=PRIVATE_SECTION_COPY CANVAS_TEST_PAGE=EXACT_PAGE_TITLE CANVA...@@ -131,7 +131,7 @@ CANVAS_TEST_SECTION=PRIVATE_SECTION_COPY CANVAS_TEST_PAGE=EXACT_PAGE_TITLE CANVA
131131
132## Unsupported page content132## Unsupported page content
133133
134Pictures, files and handwriting that occupy a paragraph lay out in the outline's flow, at the paragraph indent with its spacing. A picture draws at its user-set layout size, else its intrinsic size. A file draws as OneNote does: the icon OneNote stored for it (24 pt when unsized) centered in a 54 pt column, with its name, less the extension, wrapped and centered below; a file without a stored icon keeps the empty slot. Handwriting strokes are relative to the paragraph's top-left, which the paragraph extends to reach. All three are part of the editable outline: text navigation passes over them, edits reflow around them, a selection spanning one deletes it, and Backspace or Delete does not join text across one.134Pictures, files and handwriting that occupy a paragraph lay out in the outline's flow, at the paragraph indent with its spacing. A picture draws at its user-set layout size, else its intrinsic size. A file draws as OneNote does: the icon OneNote stored for it (24 pt when unsized) centered in a 54 pt column, with its name, less the extension, wrapped and centered below; a file without a stored icon keeps the empty slot. Handwriting strokes are relative to the paragraph's top-left, which the paragraph extends to reach. All three are part of the editable outline, and a picture there selects like a page picture, resizing and deleting in place through the outline's history (it keeps its place when dragged; Left and Right move the caret to the text beside it): text navigation passes over them, edits reflow around them, a selection spanning one deletes it, and Backspace or Delete does not join text across one.
135135
136A paragraph of content the canvas cannot draw becomes a labelled placeholder box in the flow (at least 160 pt wide, its stored size when larger), and the rest of its outline stays editable. An outline the editor cannot hold, such as one with no text for the caret, draws as stored without editing. Unsupported top-level objects, titles, or outlines that cannot be drawn render as read-only placeholders in source order. Missing image payloads or dimensions produce an image-unavailable placeholder. The editor owns the imported source values for those objects; the importer’s `Unsupported` record contains identity, class and layout metadata, not an opaque copy of unparsed notebook bytes. Supported body outlines still enter the editor. If none exist, a provisional caret appears to the right of the placeholders; typing creates an annotation outline.136A paragraph of content the canvas cannot draw becomes a labelled placeholder box in the flow (at least 160 pt wide, its stored size when larger), and the rest of its outline stays editable. An outline the editor cannot hold, such as one with no text for the caret, draws as stored without editing. Unsupported top-level objects, titles, or outlines that cannot be drawn render as read-only placeholders in source order. Missing image payloads or dimensions produce an image-unavailable placeholder. The editor owns the imported source values for those objects; the importer’s `Unsupported` record contains identity, class and layout metadata, not an opaque copy of unparsed notebook bytes. Supported body outlines still enter the editor. If none exist, a provisional caret appears to the right of the placeholders; typing creates an annotation outline.
137137