| ... | ... | @@ -123,6 +123,13 @@ enum Drag { |
| 123 | 123 | grab: [f32; 2], |
| 124 | 124 | pending_press: Option<[f32; 2]>, |
| 125 | 125 | }, |
| 126 | Image { |
| 127 | id: onestore::ExGuid, |
| 128 | handle: [i8; 2], |
| 129 | /// Document point of the press. |
| 130 | press: [f32; 2], |
| 131 | pending_press: Option<[f32; 2]>, |
| 132 | }, |
| 126 | 133 | } |
| 127 | 134 | |
| 128 | 135 | #[derive(Clone, Copy)] |
| ... | ... | @@ -130,6 +137,24 @@ enum PointerFeedback<'a> { |
| 130 | 137 | Hover(onestore::ExGuid), |
| 131 | 138 | Move(onestore::ExGuid, [f32; 2]), |
| 132 | 139 | Resize(&'a TextOutline), |
| 140 | /// A picture being moved or resized, drawn at this origin and size. |
| 141 | Image(onestore::ExGuid, [f32; 2], [f32; 2]), |
| 142 | } |
| 143 | |
| 144 | /// A non-text object holding focus, which hides the text caret and suspends typing. |
| 145 | #[derive(Clone, Copy, Debug, PartialEq)] |
| 146 | enum ObjectFocus { |
| 147 | ReadOnly(usize), |
| 148 | Image(onestore::ExGuid), |
| 149 | } |
| 150 | |
| 151 | impl ObjectFocus { |
| 152 | fn read_only(self) -> Option<usize> { |
| 153 | match self { |
| 154 | Self::ReadOnly(index) => Some(index), |
| 155 | Self::Image(_) => None, |
| 156 | } |
| 157 | } |
| 133 | 158 | } |
| 134 | 159 | |
| 135 | 160 | struct State { |
| ... | ... | @@ -151,7 +176,7 @@ struct State { |
| 151 | 176 | pointer_inside: bool, |
| 152 | 177 | last_click: Option<(Instant, [f32; 2], u8)>, |
| 153 | 178 | drag: Option<Drag>, |
| 154 | | read_only_focus: Option<usize>, |
| 179 | object_focus: Option<ObjectFocus>, |
| 155 | 180 | modifiers: ModifiersState, |
| 156 | 181 | focused: bool, |
| 157 | 182 | occluded: bool, |
| ... | ... | @@ -310,7 +335,7 @@ impl State { |
| 310 | 335 | pointer_inside: false, |
| 311 | 336 | last_click: None, |
| 312 | 337 | drag: None, |
| 313 | | read_only_focus: None, |
| 338 | object_focus: None, |
| 314 | 339 | modifiers: ModifiersState::empty(), |
| 315 | 340 | focused: true, |
| 316 | 341 | occluded: false, |
| ... | ... | @@ -352,15 +377,75 @@ impl State { |
| 352 | 377 | )) |
| 353 | 378 | } |
| 354 | 379 | |
| 355 | | fn set_read_only_focus(&mut self, index: Option<usize>) { |
| 356 | | if self.read_only_focus != index { |
| 380 | fn image_preview(&self) -> Option<(onestore::ExGuid, [f32; 2], [f32; 2])> { |
| 381 | let Drag::Image { |
| 382 | id, |
| 383 | handle, |
| 384 | press, |
| 385 | pending_press: None, |
| 386 | } = self.drag.as_ref()? |
| 387 | else { |
| 388 | return None; |
| 389 | }; |
| 390 | let (origin, size) = self.editor.image_placement(*id)?; |
| 391 | let point = self.viewport.document_point(self.pointer); |
| 392 | let delta = [point[0] - press[0], point[1] - press[1]]; |
| 393 | if *handle != [0, 0] { |
| 394 | let (origin, size) = resize_image(origin, size, *handle, delta); |
| 395 | return Some((*id, origin, size)); |
| 396 | } |
| 397 | let origin = [origin[0] + delta[0], origin[1] + delta[1]]; |
| 398 | let origin = if self.modifiers.alt_key() { |
| 399 | origin |
| 400 | } else { |
| 401 | snap_to_grid(origin) |
| 402 | }; |
| 403 | Some((*id, origin, size)) |
| 404 | } |
| 405 | |
| 406 | /// The selected picture's handles lie above the page; its body keeps its paint order. |
| 407 | fn hit_test(&self, point: [f32; 2]) -> Option<Hit> { |
| 408 | let pixel = self.display_scale / self.viewport.scale; |
| 409 | if let Some(ObjectFocus::Image(id)) = self.object_focus |
| 410 | && let Some((origin, size)) = self.editor.image_placement(id) |
| 411 | && let Some(handle) = image_handle_at(image_rect(origin, size), pixel, point) |
| 412 | { |
| 413 | return Some(Hit::Image { id, handle }); |
| 414 | } |
| 415 | page_hit_test(&self.editor, self.scene.as_ref(), point, pixel) |
| 416 | } |
| 417 | |
| 418 | fn set_object_focus(&mut self, focus: Option<ObjectFocus>) { |
| 419 | if self.object_focus != focus { |
| 357 | 420 | self.editor.finish_composition(); |
| 358 | 421 | self.drag = None; |
| 359 | | self.read_only_focus = index; |
| 360 | | self.window.set_ime_allowed(index.is_none()); |
| 422 | self.object_focus = focus; |
| 423 | self.window.set_ime_allowed(focus.is_none()); |
| 361 | 424 | } |
| 362 | 425 | } |
| 363 | 426 | |
| 427 | /// Page coordinates of a focused object. |
| 428 | fn object_rect(&self, focus: ObjectFocus) -> [f32; 4] { |
| 429 | let (scene, offset) = self.scene.as_ref().unwrap(); |
| 430 | let [x0, y0, x1, y1] = match focus { |
| 431 | ObjectFocus::ReadOnly(index) => scene |
| 432 | .read_only(Some(&self.editor)) |
| 433 | .nth(index) |
| 434 | .unwrap() |
| 435 | .rect(), |
| 436 | ObjectFocus::Image(id) => { |
| 437 | let (origin, size) = self.editor.image_placement(id).unwrap(); |
| 438 | image_rect(origin, size) |
| 439 | } |
| 440 | }; |
| 441 | [ |
| 442 | x0 + offset[0], |
| 443 | y0 + offset[1], |
| 444 | x1 + offset[0], |
| 445 | y1 + offset[1], |
| 446 | ] |
| 447 | } |
| 448 | |
| 364 | 449 | fn changed(&mut self) -> Result<(), Box<dyn Error>> { |
| 365 | 450 | self.scroll().clamp(&mut self.viewport); |
| 366 | 451 | self.caret = true; |
| ... | ... | @@ -446,7 +531,7 @@ impl State { |
| 446 | 531 | self.editor = editor; |
| 447 | 532 | self.scene = Some((scene, [0.0; 2])); |
| 448 | 533 | self.drag = None; |
| 449 | | self.read_only_focus = None; |
| 534 | self.object_focus = None; |
| 450 | 535 | self.window.set_title(&session.window_title()); |
| 451 | 536 | self.update_accessibility()?; |
| 452 | 537 | self.window.request_redraw(); |
| ... | ... | @@ -535,7 +620,7 @@ impl State { |
| 535 | 620 | self.scene.as_ref(), |
| 536 | 621 | &self.editor, |
| 537 | 622 | self.viewport, |
| 538 | | self.read_only_focus, |
| 623 | self.object_focus.and_then(ObjectFocus::read_only), |
| 539 | 624 | ); |
| 540 | 625 | update |
| 541 | 626 | } |
| ... | ... | @@ -573,7 +658,7 @@ impl State { |
| 573 | 658 | } |
| 574 | 659 | if let Some(index) = self.accessibility.read_only_for_node(request.target_node) { |
| 575 | 660 | if request.action == Action::Focus { |
| 576 | | self.set_read_only_focus(Some(index)); |
| 661 | self.set_object_focus(Some(ObjectFocus::ReadOnly(index))); |
| 577 | 662 | self.window.focus_window(); |
| 578 | 663 | self.reveal_focus()?; |
| 579 | 664 | self.changed()?; |
| ... | ... | @@ -605,7 +690,7 @@ impl State { |
| 605 | 690 | } |
| 606 | 691 | _ => return Ok(()), |
| 607 | 692 | } |
| 608 | | self.set_read_only_focus(None); |
| 693 | self.set_object_focus(None); |
| 609 | 694 | self.reveal_focus()?; |
| 610 | 695 | self.changed() |
| 611 | 696 | } |
| ... | ... | @@ -614,19 +699,9 @@ impl State { |
| 614 | 699 | if self.viewport.size.contains(&0) { |
| 615 | 700 | return Ok(()); |
| 616 | 701 | } |
| 617 | | let rect = if let Some(index) = self.read_only_focus { |
| 618 | | let (scene, offset) = self.scene.as_ref().unwrap(); |
| 619 | | let [x0, y0, x1, y1] = scene |
| 620 | | .read_only(Some(&self.editor)) |
| 621 | | .nth(index) |
| 622 | | .unwrap() |
| 623 | | .rect(); |
| 624 | | parley::BoundingBox { |
| 625 | | x0: f64::from(x0 + offset[0]), |
| 626 | | y0: f64::from(y0 + offset[1]), |
| 627 | | x1: f64::from(x1 + offset[0]), |
| 628 | | y1: f64::from(y1 + offset[1]), |
| 629 | | } |
| 702 | let rect = if let Some(focus) = self.object_focus { |
| 703 | let [x0, y0, x1, y1] = self.object_rect(focus).map(f64::from); |
| 704 | parley::BoundingBox { x0, y0, x1, y1 } |
| 630 | 705 | } else { |
| 631 | 706 | let mut rect = self.editor.caret(1.0)?; |
| 632 | 707 | let origin = self.editor.active_outline().origin(); |
| ... | ... | @@ -647,7 +722,7 @@ impl State { |
| 647 | 722 | [(outline.x0, outline.x1), (outline.y0, outline.y1)][axis]; |
| 648 | 723 | let outline_start = outline_start.min(start); |
| 649 | 724 | let outline_end = outline_end.max(end); |
| 650 | | if self.read_only_focus.is_none() |
| 725 | if self.object_focus.is_none() |
| 651 | 726 | && (outline_end - outline_start) * f64::from(self.viewport.scale) |
| 652 | 727 | <= size - margin * 2.0 |
| 653 | 728 | { |
| ... | ... | @@ -759,6 +834,10 @@ impl State { |
| 759 | 834 | _ => self |
| 760 | 835 | .preview() |
| 761 | 836 | .map(|(id, origin)| PointerFeedback::Move(id, origin)) |
| 837 | .or_else(|| { |
| 838 | self.image_preview() |
| 839 | .map(|(id, origin, size)| PointerFeedback::Image(id, origin, size)) |
| 840 | }) |
| 762 | 841 | .or_else(|| { |
| 763 | 842 | if !self.pointer_inside { |
| 764 | 843 | return None; |
| ... | ... | @@ -778,10 +857,13 @@ impl State { |
| 778 | 857 | } |
| 779 | 858 | }), |
| 780 | 859 | }, |
| 781 | | self.read_only_focus, |
| 860 | self.object_focus, |
| 782 | 861 | self.caret |
| 783 | 862 | && self.focused |
| 784 | | && !matches!(self.drag, Some(Drag::Outline { .. } | Drag::Resize { .. })), |
| 863 | && !matches!( |
| 864 | self.drag, |
| 865 | Some(Drag::Outline { .. } | Drag::Resize { .. } | Drag::Image { .. }) |
| 866 | ), |
| 785 | 867 | self.viewport.scale, |
| 786 | 868 | self.display_scale, |
| 787 | 869 | )?; |
| ... | ... | @@ -807,12 +889,15 @@ impl State { |
| 807 | 889 | let shift = self.modifiers.shift_key(); |
| 808 | 890 | let command = self.modifiers.super_key(); |
| 809 | 891 | let option = self.modifiers.alt_key(); |
| 810 | | if self.read_only_focus.is_some() { |
| 811 | | if !read_only_shortcut(key, self.modifiers) { |
| 892 | if let Some(focus) = self.object_focus { |
| 893 | let undo = matches!(focus, ObjectFocus::Image(_)) |
| 894 | && command |
| 895 | && matches!(key, Key::Character(value) if value.eq_ignore_ascii_case("z")); |
| 896 | if !undo && !read_only_shortcut(key, self.modifiers) { |
| 812 | 897 | return Ok(()); |
| 813 | 898 | } |
| 814 | 899 | if key == &Key::Named(NamedKey::Escape) { |
| 815 | | self.set_read_only_focus(None); |
| 900 | self.set_object_focus(None); |
| 816 | 901 | self.reveal_focus()?; |
| 817 | 902 | return self.changed(); |
| 818 | 903 | } |
| ... | ... | @@ -832,7 +917,10 @@ impl State { |
| 832 | 917 | _ => {} |
| 833 | 918 | } |
| 834 | 919 | } |
| 835 | | if matches!(self.drag, Some(Drag::Outline { .. } | Drag::Resize { .. })) { |
| 920 | if matches!( |
| 921 | self.drag, |
| 922 | Some(Drag::Outline { .. } | Drag::Resize { .. } | Drag::Image { .. }) |
| 923 | ) { |
| 836 | 924 | if matches!( |
| 837 | 925 | key, |
| 838 | 926 | Key::Named( |
| ... | ... | @@ -881,14 +969,17 @@ impl State { |
| 881 | 969 | if count == 0 { |
| 882 | 970 | return self.changed(); |
| 883 | 971 | } |
| 884 | | let index = self.read_only_focus.map_or_else( |
| 885 | | || { |
| 886 | | outlines |
| 887 | | .iter() |
| 888 | | .position(|outline| outline.id == self.editor.active_outline().id) |
| 889 | | }, |
| 890 | | |index| Some(outlines.len() + index), |
| 891 | | ); |
| 972 | let index = self |
| 973 | .object_focus |
| 974 | .and_then(ObjectFocus::read_only) |
| 975 | .map_or_else( |
| 976 | || { |
| 977 | outlines |
| 978 | .iter() |
| 979 | .position(|outline| outline.id == self.editor.active_outline().id) |
| 980 | }, |
| 981 | |index| Some(outlines.len() + index), |
| 982 | ); |
| 892 | 983 | let next = index.map_or(if shift { count - 1 } else { 0 }, |index| { |
| 893 | 984 | if shift { |
| 894 | 985 | (index + count - 1) % count |
| ... | ... | @@ -898,9 +989,9 @@ impl State { |
| 898 | 989 | }); |
| 899 | 990 | if next < outlines.len() { |
| 900 | 991 | self.editor.focus_outline(outlines[next].id)?; |
| 901 | | self.set_read_only_focus(None); |
| 992 | self.set_object_focus(None); |
| 902 | 993 | } else { |
| 903 | | self.set_read_only_focus(Some(next - outlines.len())); |
| 994 | self.set_object_focus(Some(ObjectFocus::ReadOnly(next - outlines.len()))); |
| 904 | 995 | } |
| 905 | 996 | self.reveal_focus()?; |
| 906 | 997 | return self.changed(); |
| ... | ... | @@ -908,14 +999,9 @@ impl State { |
| 908 | 999 | if command && let Key::Character(key) = key { |
| 909 | 1000 | match key.to_lowercase().as_str() { |
| 910 | 1001 | "n" if shift => { |
| 911 | | let position = if let Some(index) = self.read_only_focus { |
| 912 | | let (scene, offset) = self.scene.as_ref().unwrap(); |
| 913 | | let rect = scene |
| 914 | | .read_only(Some(&self.editor)) |
| 915 | | .nth(index) |
| 916 | | .unwrap() |
| 917 | | .rect(); |
| 918 | | [rect[2] + offset[0] + 24.0, rect[1] + offset[1]] |
| 1002 | let position = if let Some(focus) = self.object_focus { |
| 1003 | let rect = self.object_rect(focus); |
| 1004 | [rect[2] + 24.0, rect[1]] |
| 919 | 1005 | } else { |
| 920 | 1006 | let bounds = self.editor.active_outline().bounds(); |
| 921 | 1007 | [bounds.x1 as f32 + 24.0, bounds.y0 as f32] |
| ... | ... | @@ -925,7 +1011,7 @@ impl State { |
| 925 | 1011 | snap_to_grid(position), |
| 926 | 1012 | DEFAULT_OUTLINE_WIDTH, |
| 927 | 1013 | )?; |
| 928 | | self.set_read_only_focus(None); |
| 1014 | self.set_object_focus(None); |
| 929 | 1015 | } |
| 930 | 1016 | "a" => self.editor.select_all()?, |
| 931 | 1017 | "z" => { |
| ... | ... | @@ -1137,7 +1223,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1137 | 1223 | let event = match event { |
| 1138 | 1224 | UserEvent::InsertText(text) => { |
| 1139 | 1225 | if let Some(state) = &mut self.state |
| 1140 | | && state.read_only_focus.is_none() |
| 1226 | && state.object_focus.is_none() |
| 1141 | 1227 | && !text.is_empty() |
| 1142 | 1228 | && !text.chars().any(char::is_control) |
| 1143 | 1229 | { |
| ... | ... | @@ -1282,12 +1368,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1282 | 1368 | WindowEvent::CursorMoved { position, .. } => { |
| 1283 | 1369 | state.pointer_inside = true; |
| 1284 | 1370 | state.pointer = [position.x as f32, position.y as f32]; |
| 1285 | | let hit = page_hit_test( |
| 1286 | | &state.editor, |
| 1287 | | state.scene.as_ref(), |
| 1288 | | state.viewport.document_point(state.pointer), |
| 1289 | | state.display_scale / state.viewport.scale, |
| 1290 | | ); |
| 1371 | let hit = state.hit_test(state.viewport.document_point(state.pointer)); |
| 1291 | 1372 | let scrollbar = state |
| 1292 | 1373 | .scroll() |
| 1293 | 1374 | .hit_test(state.viewport, state.display_scale, state.pointer) |
| ... | ... | @@ -1295,6 +1376,8 @@ impl ApplicationHandler<UserEvent> for App { |
| 1295 | 1376 | state.window.set_cursor(match (&state.drag, hit) { |
| 1296 | 1377 | (Some(Drag::Scrollbar { .. }), _) => winit::window::CursorIcon::Default, |
| 1297 | 1378 | (None, _) if scrollbar => winit::window::CursorIcon::Default, |
| 1379 | (Some(Drag::Image { handle, .. }), _) => handle_cursor(*handle), |
| 1380 | (None, Some(Hit::Image { handle, .. })) => handle_cursor(handle), |
| 1298 | 1381 | (Some(Drag::Resize { .. }), _) | (None, Some(Hit::Resize { .. })) => { |
| 1299 | 1382 | winit::window::CursorIcon::EwResize |
| 1300 | 1383 | } |
| ... | ... | @@ -1330,7 +1413,9 @@ impl ApplicationHandler<UserEvent> for App { |
| 1330 | 1413 | .select(drag_selection(*anchor, target, *unit))?; |
| 1331 | 1414 | state.changed()?; |
| 1332 | 1415 | } |
| 1333 | | Some(Drag::Outline { pending_press, .. }) => { |
| 1416 | Some( |
| 1417 | Drag::Outline { pending_press, .. } | Drag::Image { pending_press, .. }, |
| 1418 | ) => { |
| 1334 | 1419 | if pending_press.is_some_and(|press| { |
| 1335 | 1420 | (0..2).any(|axis| { |
| 1336 | 1421 | (state.pointer[axis] - press[axis]).abs() |
| ... | ... | @@ -1387,18 +1472,22 @@ impl ApplicationHandler<UserEvent> for App { |
| 1387 | 1472 | if scrollbar.is_some() { |
| 1388 | 1473 | state.drag = scrollbar; |
| 1389 | 1474 | } else { |
| 1390 | | match page_hit_test( |
| 1391 | | &state.editor, |
| 1392 | | state.scene.as_ref(), |
| 1393 | | point, |
| 1394 | | state.display_scale / state.viewport.scale, |
| 1395 | | ) { |
| 1475 | match state.hit_test(point) { |
| 1396 | 1476 | Some(Hit::Date(field)) => state.edit_date(field)?, |
| 1397 | 1477 | Some(Hit::ReadOnly(index)) => { |
| 1398 | | state.set_read_only_focus(Some(index)) |
| 1478 | state.set_object_focus(Some(ObjectFocus::ReadOnly(index))) |
| 1479 | } |
| 1480 | Some(Hit::Image { id, handle }) => { |
| 1481 | state.set_object_focus(Some(ObjectFocus::Image(id))); |
| 1482 | state.drag = Some(Drag::Image { |
| 1483 | id, |
| 1484 | handle, |
| 1485 | press: point, |
| 1486 | pending_press: Some(state.pointer), |
| 1487 | }); |
| 1399 | 1488 | } |
| 1400 | 1489 | Some(Hit::Handle { id, grab }) => { |
| 1401 | | state.set_read_only_focus(None); |
| 1490 | state.set_object_focus(None); |
| 1402 | 1491 | state.editor.focus_outline(id)?; |
| 1403 | 1492 | state.drag = Some(Drag::Outline { |
| 1404 | 1493 | id, |
| ... | ... | @@ -1407,7 +1496,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1407 | 1496 | }); |
| 1408 | 1497 | } |
| 1409 | 1498 | Some(Hit::Resize { id, grab }) => { |
| 1410 | | state.set_read_only_focus(None); |
| 1499 | state.set_object_focus(None); |
| 1411 | 1500 | state.editor.focus_outline(id)?; |
| 1412 | 1501 | state.drag = Some(Drag::Resize { |
| 1413 | 1502 | outline: None, |
| ... | ... | @@ -1416,9 +1505,9 @@ impl ApplicationHandler<UserEvent> for App { |
| 1416 | 1505 | } |
| 1417 | 1506 | Some(Hit::Text { id, point }) => { |
| 1418 | 1507 | let extend = state.modifiers.shift_key() |
| 1419 | | && state.read_only_focus.is_none() |
| 1508 | && state.object_focus.is_none() |
| 1420 | 1509 | && id == state.editor.active_outline().id; |
| 1421 | | state.set_read_only_focus(None); |
| 1510 | state.set_object_focus(None); |
| 1422 | 1511 | state.editor.focus_outline(id)?; |
| 1423 | 1512 | let previous = state.editor.selection(); |
| 1424 | 1513 | state.editor.select_below(&mut state.engine, id, point)?; |
| ... | ... | @@ -1464,7 +1553,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1464 | 1553 | position, |
| 1465 | 1554 | DEFAULT_OUTLINE_WIDTH, |
| 1466 | 1555 | )?; |
| 1467 | | state.set_read_only_focus(None); |
| 1556 | state.set_object_focus(None); |
| 1468 | 1557 | state.drag = Some(Drag::Text { |
| 1469 | 1558 | anchor: state.editor.selection(), |
| 1470 | 1559 | unit: SelectionUnit::Grapheme, |
| ... | ... | @@ -1474,6 +1563,9 @@ impl ApplicationHandler<UserEvent> for App { |
| 1474 | 1563 | } |
| 1475 | 1564 | } else { |
| 1476 | 1565 | let preview = state.preview(); |
| 1566 | if let Some((id, origin, size)) = state.image_preview() { |
| 1567 | state.editor.place_image(id, origin, size)?; |
| 1568 | } |
| 1477 | 1569 | if let Some(Drag::Resize { |
| 1478 | 1570 | outline: Some(outline), |
| 1479 | 1571 | .. |
| ... | ... | @@ -1508,7 +1600,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1508 | 1600 | { |
| 1509 | 1601 | state.key(&event.logical_key, event.text.as_deref())?; |
| 1510 | 1602 | } |
| 1511 | | WindowEvent::Ime(_) if state.read_only_focus.is_some() => {} |
| 1603 | WindowEvent::Ime(_) if state.object_focus.is_some() => {} |
| 1512 | 1604 | WindowEvent::Ime(Ime::Preedit(text, cursor)) => { |
| 1513 | 1605 | if text.is_empty() { |
| 1514 | 1606 | state.editor.cancel_composition(&mut state.engine)?; |
| ... | ... | @@ -1558,7 +1650,7 @@ impl ApplicationHandler<UserEvent> for App { |
| 1558 | 1650 | return; |
| 1559 | 1651 | }; |
| 1560 | 1652 | let [anchor, focus] = state.editor.selection().positions; |
| 1561 | | if state.focused && !state.occluded && state.read_only_focus.is_none() && anchor == focus { |
| 1653 | if state.focused && !state.occluded && state.object_focus.is_none() && anchor == focus { |
| 1562 | 1654 | let now = Instant::now(); |
| 1563 | 1655 | if now >= state.blink_at { |
| 1564 | 1656 | state.caret = !state.caret; |
| ... | ... | @@ -1588,6 +1680,11 @@ enum Hit { |
| 1588 | 1680 | grab: [f32; 2], |
| 1589 | 1681 | }, |
| 1590 | 1682 | ReadOnly(usize), |
| 1683 | /// `handle` is [0, 0] on the picture and a direction on the selected picture's handles. |
| 1684 | Image { |
| 1685 | id: onestore::ExGuid, |
| 1686 | handle: [i8; 2], |
| 1687 | }, |
| 1591 | 1688 | } |
| 1592 | 1689 | |
| 1593 | 1690 | fn page_hit_test( |
| ... | ... | @@ -1696,6 +1793,7 @@ fn page_hit_test( |
| 1696 | 1793 | canvas::gpu::page::SceneHit::Outline(hit) => Some(hit), |
| 1697 | 1794 | canvas::gpu::page::SceneHit::Date(field) => Some(Hit::Date(field)), |
| 1698 | 1795 | canvas::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), |
| 1796 | canvas::gpu::page::SceneHit::Image(id) => Some(Hit::Image { id, handle: [0, 0] }), |
| 1699 | 1797 | } |
| 1700 | 1798 | }; |
| 1701 | 1799 | [Layer::Grips, Layer::Body, Layer::Below] |
| ... | ... | @@ -1707,7 +1805,7 @@ fn page_primitives<'a>( |
| 1707 | 1805 | editor: &'a CanvasEditor, |
| 1708 | 1806 | scene: Option<&'a (PageScene, [f32; 2])>, |
| 1709 | 1807 | preview: Option<PointerFeedback<'a>>, |
| 1710 | | read_only_focus: Option<usize>, |
| 1808 | object_focus: Option<ObjectFocus>, |
| 1711 | 1809 | show_caret: bool, |
| 1712 | 1810 | scale: f32, |
| 1713 | 1811 | display_scale: f32, |
| ... | ... | @@ -1726,7 +1824,7 @@ fn page_primitives<'a>( |
| 1726 | 1824 | Some(PointerFeedback::Move(id, origin)) if id == outline.id => origin, |
| 1727 | 1825 | _ => outline.origin(), |
| 1728 | 1826 | }; |
| 1729 | | if (read_only_focus.is_none() && outline.id == editor.active_outline().id) |
| 1827 | if (object_focus.is_none() && outline.id == editor.active_outline().id) |
| 1730 | 1828 | || matches!(preview, Some(PointerFeedback::Hover(id) | PointerFeedback::Move(id, _)) if id == outline.id) |
| 1731 | 1829 | || matches!(preview, Some(PointerFeedback::Resize(resized)) if resized.id == outline.id) |
| 1732 | 1830 | { |
| ... | ... | @@ -1738,7 +1836,7 @@ fn page_primitives<'a>( |
| 1738 | 1836 | ); |
| 1739 | 1837 | } |
| 1740 | 1838 | append_outline( |
| 1741 | | (read_only_focus.is_none() |
| 1839 | (object_focus.is_none() |
| 1742 | 1840 | && outline.id == editor.active_outline().id |
| 1743 | 1841 | && !matches!(preview, Some(PointerFeedback::Resize(_)))) |
| 1744 | 1842 | .then_some(editor), |
| ... | ... | @@ -1751,7 +1849,11 @@ fn page_primitives<'a>( |
| 1751 | 1849 | ) |
| 1752 | 1850 | }; |
| 1753 | 1851 | if let Some((scene, origin)) = scene { |
| 1754 | | scene.append_primitives_with(&mut primitives, *origin, Some(editor), &paint)?; |
| 1852 | let moving = match preview { |
| 1853 | Some(PointerFeedback::Image(id, origin, size)) => Some((id, image_rect(origin, size))), |
| 1854 | _ => None, |
| 1855 | }; |
| 1856 | scene.append_primitives_with(&mut primitives, *origin, Some(editor), moving, &paint)?; |
| 1755 | 1857 | } |
| 1756 | 1858 | for outline in editor |
| 1757 | 1859 | .visible_outlines() |
| ... | ... | @@ -1759,7 +1861,7 @@ fn page_primitives<'a>( |
| 1759 | 1861 | { |
| 1760 | 1862 | paint(outline.id, [0.0; 2], &mut primitives)?; |
| 1761 | 1863 | } |
| 1762 | | if read_only_focus.is_none() |
| 1864 | if object_focus.is_none() |
| 1763 | 1865 | && let Some(outline) = editor.caret_outline() |
| 1764 | 1866 | { |
| 1765 | 1867 | append_outline( |
| ... | ... | @@ -1772,7 +1874,20 @@ fn page_primitives<'a>( |
| 1772 | 1874 | &mut primitives, |
| 1773 | 1875 | )?; |
| 1774 | 1876 | } |
| 1775 | | if let Some(index) = read_only_focus { |
| 1877 | if let Some(ObjectFocus::Image(id)) = object_focus { |
| 1878 | let (origin, size) = match preview { |
| 1879 | Some(PointerFeedback::Image(moving, origin, size)) if moving == id => (origin, size), |
| 1880 | _ => editor |
| 1881 | .image_placement(id) |
| 1882 | .ok_or("The selected picture is missing.")?, |
| 1883 | }; |
| 1884 | append_image_chrome( |
| 1885 | image_rect(origin, size), |
| 1886 | display_scale / scale, |
| 1887 | &mut primitives, |
| 1888 | ); |
| 1889 | } |
| 1890 | if let Some(ObjectFocus::ReadOnly(index)) = object_focus { |
| 1776 | 1891 | let (scene, offset) = scene.unwrap(); |
| 1777 | 1892 | let [x0, y0, x1, y1] = scene.read_only(Some(editor)).nth(index).unwrap().rect(); |
| 1778 | 1893 | let [x0, y0, x1, y1] = [ |
| ... | ... | @@ -1797,6 +1912,124 @@ fn page_primitives<'a>( |
| 1797 | 1912 | Ok(primitives) |
| 1798 | 1913 | } |
| 1799 | 1914 | |
| 1915 | /// Native picture handles sit on a selection border drawn 5 px outside the picture, named |
| 1916 | /// by their direction from its center. |
| 1917 | fn image_handles(rect: [f32; 4], pixel: f32) -> impl Iterator<Item = ([i8; 2], [f32; 2])> { |
| 1918 | let border = [ |
| 1919 | rect[0] - 5.0 * pixel, |
| 1920 | rect[1] - 5.0 * pixel, |
| 1921 | rect[2] + 5.0 * pixel, |
| 1922 | rect[3] + 5.0 * pixel, |
| 1923 | ]; |
| 1924 | [-1, 0, 1] |
| 1925 | .into_iter() |
| 1926 | .flat_map(|y| [-1, 0, 1].map(|x| [x, y])) |
| 1927 | .filter(|handle| *handle != [0, 0]) |
| 1928 | .map(move |handle| { |
| 1929 | ( |
| 1930 | handle, |
| 1931 | std::array::from_fn(|axis| { |
| 1932 | let [start, end] = [border[axis], border[axis + 2]]; |
| 1933 | start + (end - start) * f32::from(handle[axis] + 1) * 0.5 |
| 1934 | }), |
| 1935 | ) |
| 1936 | }) |
| 1937 | } |
| 1938 | |
| 1939 | fn image_handle_at(rect: [f32; 4], pixel: f32, point: [f32; 2]) -> Option<[i8; 2]> { |
| 1940 | image_handles(rect, pixel) |
| 1941 | .find(|(_, center)| (0..2).all(|axis| (point[axis] - center[axis]).abs() <= 5.0 * pixel)) |
| 1942 | .map(|(handle, _)| handle) |
| 1943 | } |
| 1944 | |
| 1945 | fn handle_cursor(handle: [i8; 2]) -> winit::window::CursorIcon { |
| 1946 | use winit::window::CursorIcon; |
| 1947 | match handle { |
| 1948 | [0, 0] => CursorIcon::Move, |
| 1949 | [_, 0] => CursorIcon::EwResize, |
| 1950 | [0, _] => CursorIcon::NsResize, |
| 1951 | [x, y] if x == y => CursorIcon::NwseResize, |
| 1952 | _ => CursorIcon::NeswResize, |
| 1953 | } |
| 1954 | } |
| 1955 | |
| 1956 | fn image_rect(origin: [f32; 2], size: [f32; 2]) -> [f32; 4] { |
| 1957 | [ |
| 1958 | origin[0], |
| 1959 | origin[1], |
| 1960 | origin[0] + size[0], |
| 1961 | origin[1] + size[1], |
| 1962 | ] |
| 1963 | } |
| 1964 | |
| 1965 | /// Drags a picture's `handle` by `delta`: edges stretch one axis, corners keep the aspect |
| 1966 | /// ratio at the larger of the two scales, and the opposite side stays fixed. |
| 1967 | fn resize_image( |
| 1968 | origin: [f32; 2], |
| 1969 | size: [f32; 2], |
| 1970 | handle: [i8; 2], |
| 1971 | delta: [f32; 2], |
| 1972 | ) -> ([f32; 2], [f32; 2]) { |
| 1973 | let mut scale: [f32; 2] = std::array::from_fn(|axis| { |
| 1974 | (size[axis] + f32::from(handle[axis]) * delta[axis]) / size[axis] |
| 1975 | }); |
| 1976 | if !handle.contains(&0) { |
| 1977 | scale = [scale[0].max(scale[1]); 2]; |
| 1978 | } |
| 1979 | let resized: [f32; 2] = std::array::from_fn(|axis| (size[axis] * scale[axis]).max(1.0)); |
| 1980 | let origin = std::array::from_fn(|axis| { |
| 1981 | if handle[axis] < 0 { |
| 1982 | origin[axis] + size[axis] - resized[axis] |
| 1983 | } else { |
| 1984 | origin[axis] |
| 1985 | } |
| 1986 | }); |
| 1987 | (origin, resized) |
| 1988 | } |
| 1989 | |
| 1990 | fn append_image_chrome(rect: [f32; 4], pixel: f32, primitives: &mut Vec<Primitive<'_>>) { |
| 1991 | let mut tint = canvas::gpu::colorref(0x00e0d2e6); |
| 1992 | // Native pictures take this tint at 25% in sRGB; 10% in linear light matches it. |
| 1993 | tint[3] = 0.1; |
| 1994 | primitives.push(Primitive::Rect { rect, color: tint }); |
| 1995 | primitives.push(Primitive::RoundedRect { |
| 1996 | rect: [ |
| 1997 | rect[0] - 5.0 * pixel, |
| 1998 | rect[1] - 5.0 * pixel, |
| 1999 | rect[2] + 5.0 * pixel, |
| 2000 | rect[3] + 5.0 * pixel, |
| 2001 | ], |
| 2002 | radius: [0.0; 2], |
| 2003 | stroke: Some(Stroke::Dashed(pixel)), |
| 2004 | color: canvas::gpu::colorref(0x00ff9a31), |
| 2005 | }); |
| 2006 | for (handle, [x, y]) in image_handles(rect, pixel) { |
| 2007 | let (half, radius) = if handle.contains(&0) { |
| 2008 | (3.5, 0.0) |
| 2009 | } else { |
| 2010 | (4.0, 4.0) |
| 2011 | }; |
| 2012 | let rect = [ |
| 2013 | x - half * pixel, |
| 2014 | y - half * pixel, |
| 2015 | x + half * pixel, |
| 2016 | y + half * pixel, |
| 2017 | ]; |
| 2018 | primitives.push(Primitive::RoundedRect { |
| 2019 | rect, |
| 2020 | radius: [radius * pixel; 2], |
| 2021 | stroke: None, |
| 2022 | color: canvas::gpu::colorref(0x00ffefe7), |
| 2023 | }); |
| 2024 | primitives.push(Primitive::RoundedRect { |
| 2025 | rect, |
| 2026 | radius: [radius * pixel; 2], |
| 2027 | stroke: Some(Stroke::Solid(pixel)), |
| 2028 | color: canvas::gpu::colorref(0x00dea67b), |
| 2029 | }); |
| 2030 | } |
| 2031 | } |
| 2032 | |
| 1800 | 2033 | fn drag_selection(anchor: Selection, target: Selection, unit: SelectionUnit) -> Selection { |
| 1801 | 2034 | if unit == SelectionUnit::Grapheme { |
| 1802 | 2035 | return Selection { |
| ... | ... | @@ -2500,6 +2733,33 @@ mod tests { |
| 2500 | 2733 | ); |
| 2501 | 2734 | } |
| 2502 | 2735 | |
| 2736 | #[test] |
| 2737 | fn picture_handles_resize_as_onenote_does() { |
| 2738 | // Native drags of the 333 x 200.1 pt mockup on "av: casual night in the trees". |
| 2739 | let (origin, size) = ([468.0, 86.4], [333.0, 200.1]); |
| 2740 | let (corner_origin, corner) = resize_image(origin, size, [1, -1], [-75.0, 30.75]); |
| 2741 | assert!((corner[0] - 281.827).abs() < 0.01 && (corner[1] - 169.351).abs() < 0.01); |
| 2742 | assert!((corner_origin[1] - 117.15).abs() < 0.01 && corner_origin[0] == 468.0); |
| 2743 | assert_eq!( |
| 2744 | resize_image(origin, size, [1, 0], [46.5, 10.0]), |
| 2745 | (origin, [379.5, 200.1]) |
| 2746 | ); |
| 2747 | let (left_origin, left) = resize_image(origin, size, [-1, 0], [400.0, 0.0]); |
| 2748 | assert_eq!(left, [1.0, 200.1]); |
| 2749 | assert_eq!(left_origin[0], 468.0 + 333.0 - 1.0); |
| 2750 | let pixel = 0.75; |
| 2751 | let rect = image_rect(origin, size); |
| 2752 | assert_eq!( |
| 2753 | image_handle_at(rect, pixel, [468.0 - 3.75, 86.4 - 3.75]), |
| 2754 | Some([-1, -1]) |
| 2755 | ); |
| 2756 | assert_eq!( |
| 2757 | image_handle_at(rect, pixel, [468.0 + 166.5, 286.5 + 3.75]), |
| 2758 | Some([0, 1]) |
| 2759 | ); |
| 2760 | assert_eq!(image_handle_at(rect, pixel, [600.0, 150.0]), None); |
| 2761 | } |
| 2762 | |
| 2503 | 2763 | #[test] |
| 2504 | 2764 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { |
| 2505 | 2765 | let selection = |start, end| { |
| ... | ... | @@ -3257,7 +3517,16 @@ mod tests { |
| 3257 | 3517 | .any(|(_, color)| *color == macos::text_colors()[0]) |
| 3258 | 3518 | ); |
| 3259 | 3519 | let selected = rectangles( |
| 3260 | | page_primitives(&editor, Some(&scene), None, Some(0), true, scale, 1.0).unwrap(), |
| 3520 | page_primitives( |
| 3521 | &editor, |
| 3522 | Some(&scene), |
| 3523 | None, |
| 3524 | Some(ObjectFocus::ReadOnly(0)), |
| 3525 | true, |
| 3526 | scale, |
| 3527 | 1.0, |
| 3528 | ) |
| 3529 | .unwrap(), |
| 3261 | 3530 | ); |
| 3262 | 3531 | assert!( |
| 3263 | 3532 | !selected |
| ... | ... | @@ -3267,8 +3536,16 @@ mod tests { |
| 3267 | 3536 | assert_eq!( |
| 3268 | 3537 | selected, |
| 3269 | 3538 | rectangles( |
| 3270 | | page_primitives(&editor, Some(&scene), None, Some(0), false, scale, 1.0) |
| 3271 | | .unwrap() |
| 3539 | page_primitives( |
| 3540 | &editor, |
| 3541 | Some(&scene), |
| 3542 | None, |
| 3543 | Some(ObjectFocus::ReadOnly(0)), |
| 3544 | false, |
| 3545 | scale, |
| 3546 | 1.0 |
| 3547 | ) |
| 3548 | .unwrap() |
| 3272 | 3549 | ) |
| 3273 | 3550 | ); |
| 3274 | 3551 | assert_eq!( |
| ... | ... | @@ -3285,7 +3562,16 @@ mod tests { |
| 3285 | 3562 | .any(|(_, color)| *color == macos::text_colors()[1]) |
| 3286 | 3563 | ); |
| 3287 | 3564 | let selected = rectangles( |
| 3288 | | page_primitives(&editor, Some(&scene), None, Some(0), true, 1.0, 1.0).unwrap(), |
| 3565 | page_primitives( |
| 3566 | &editor, |
| 3567 | Some(&scene), |
| 3568 | None, |
| 3569 | Some(ObjectFocus::ReadOnly(0)), |
| 3570 | true, |
| 3571 | 1.0, |
| 3572 | 1.0, |
| 3573 | ) |
| 3574 | .unwrap(), |
| 3289 | 3575 | ); |
| 3290 | 3576 | assert!( |
| 3291 | 3577 | !selected |