| ... | @@ -1596,8 +1596,17 @@ fn page_hit_test( | ... | @@ -1596,8 +1596,17 @@ fn page_hit_test( |
| 1596 | point: [f32; 2], | 1596 | point: [f32; 2], |
| 1597 | pixel: f32, | 1597 | pixel: f32, |
| 1598 | ) -> Option<Hit> { | 1598 | ) -> Option<Hit> { |
| 1599 | let hit = |outline: &TextOutline, offset: [f32; 2], below: bool| { | 1599 | /// Grips and text resolve front to back before any outline's padding, as a native |
| 1600 | if below { | 1600 | /// width handle stays reachable under the next outline's left padding; the typing room |
| | 1601 | /// below an outline comes last. |
| | 1602 | #[derive(Clone, Copy, PartialEq)] |
| | 1603 | enum Layer { |
| | 1604 | Grips, |
| | 1605 | Body, |
| | 1606 | Below, |
| | 1607 | } |
| | 1608 | let hit = |outline: &TextOutline, offset: [f32; 2], layer: Layer| { |
| | 1609 | if layer == Layer::Below { |
| 1601 | let outline = editor | 1610 | let outline = editor |
| 1602 | .outlines() | 1611 | .outlines() |
| 1603 | .iter() | 1612 | .iter() |
| ... | @@ -1614,20 +1623,34 @@ fn page_hit_test( | ... | @@ -1614,20 +1623,34 @@ fn page_hit_test( |
| 1614 | let (bounds, body_top) = outline_chrome(outline, pixel); | 1623 | let (bounds, body_top) = outline_chrome(outline, pixel); |
| 1615 | let local = [point[0] - offset[0], point[1] - offset[1]]; | 1624 | let local = [point[0] - offset[0], point[1] - offset[1]]; |
| 1616 | let [x, y] = local; | 1625 | let [x, y] = local; |
| 1617 | if x >= bounds[0] && x <= bounds[2] && y >= bounds[1] && y < body_top { | 1626 | if layer == Layer::Grips { |
| 1618 | if x >= bounds[2] - 9.0 * pixel { | 1627 | // Width handles: 15 px inside to 6 px outside the header's right end, and 6 px |
| | 1628 | // either side of the right border below it. |
| | 1629 | let inside = if y < body_top { 15.0 } else { 6.0 }; |
| | 1630 | if !outline.title |
| | 1631 | && (bounds[2] - inside * pixel..=bounds[2] + 6.0 * pixel).contains(&x) |
| | 1632 | && (bounds[1]..=bounds[3]).contains(&y) |
| | 1633 | { |
| 1619 | return Some(Hit::Resize { | 1634 | return Some(Hit::Resize { |
| 1620 | id: outline.id, | 1635 | id: outline.id, |
| 1621 | grab: point[0] - outline.bounds().x1 as f32, | 1636 | grab: point[0] - outline.bounds().x1 as f32, |
| 1622 | }); | 1637 | }); |
| 1623 | } | 1638 | } |
| 1624 | return Some(Hit::Handle { | 1639 | if x >= bounds[0] && x <= bounds[2] && y >= bounds[1] && y < body_top { |
| 1625 | id: outline.id, | 1640 | return Some(Hit::Handle { |
| 1626 | grab: [ | 1641 | id: outline.id, |
| 1627 | point[0] - outline.origin()[0], | 1642 | grab: [ |
| 1628 | point[1] - outline.origin()[1], | 1643 | point[0] - outline.origin()[0], |
| 1629 | ], | 1644 | point[1] - outline.origin()[1], |
| 1630 | }); | 1645 | ], |
| | 1646 | }); |
| | 1647 | } |
| | 1648 | let text = outline.bounds(); |
| | 1649 | if !(text.x0..=text.x1).contains(&f64::from(x)) |
| | 1650 | || !(text.y0..=text.y1).contains(&f64::from(y)) |
| | 1651 | { |
| | 1652 | return None; |
| | 1653 | } |
| 1631 | } | 1654 | } |
| 1632 | if (x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3]) | 1655 | if (x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3]) |
| 1633 | || outline.layouts().any(|(_, paragraph)| { | 1656 | || outline.layouts().any(|(_, paragraph)| { |
| ... | @@ -1650,12 +1673,12 @@ fn page_hit_test( | ... | @@ -1650,12 +1673,12 @@ fn page_hit_test( |
| 1650 | } | 1673 | } |
| 1651 | None | 1674 | None |
| 1652 | }; | 1675 | }; |
| 1653 | let pass = |below| { | 1676 | let pass = |layer| { |
| 1654 | if let Some(hit) = editor | 1677 | if let Some(hit) = editor |
| 1655 | .visible_outlines() | 1678 | .visible_outlines() |
| 1656 | .rev() | 1679 | .rev() |
| 1657 | .filter(|outline| scene.is_none() || !editor.has_page_outline(outline.id)) | 1680 | .filter(|outline| scene.is_none() || !editor.has_page_outline(outline.id)) |
| 1658 | .find_map(|outline| hit(outline, [0.0; 2], below)) | 1681 | .find_map(|outline| hit(outline, [0.0; 2], layer)) |
| 1659 | { | 1682 | { |
| 1660 | return Some(hit); | 1683 | return Some(hit); |
| 1661 | } | 1684 | } |
| ... | @@ -1667,7 +1690,7 @@ fn page_hit_test( | ... | @@ -1667,7 +1690,7 @@ fn page_hit_test( |
| 1667 | editor | 1690 | editor |
| 1668 | .visible_outlines() | 1691 | .visible_outlines() |
| 1669 | .find(|outline| outline.id == id) | 1692 | .find(|outline| outline.id == id) |
| 1670 | .and_then(|outline| hit(outline, *offset, below)) | 1693 | .and_then(|outline| hit(outline, *offset, layer)) |
| 1671 | }, | 1694 | }, |
| 1672 | )? { | 1695 | )? { |
| 1673 | canvas::gpu::page::SceneHit::Outline(hit) => Some(hit), | 1696 | canvas::gpu::page::SceneHit::Outline(hit) => Some(hit), |
| ... | @@ -1675,7 +1698,9 @@ fn page_hit_test( | ... | @@ -1675,7 +1698,9 @@ fn page_hit_test( |
| 1675 | canvas::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), | 1698 | canvas::gpu::page::SceneHit::ReadOnly(index) => Some(Hit::ReadOnly(index)), |
| 1676 | } | 1699 | } |
| 1677 | }; | 1700 | }; |
| 1678 | pass(false).or_else(|| pass(true)) | 1701 | [Layer::Grips, Layer::Body, Layer::Below] |
| | 1702 | .into_iter() |
| | 1703 | .find_map(pass) |
| 1679 | } | 1704 | } |
| 1680 | | 1705 | |
| 1681 | fn page_primitives<'a>( | 1706 | fn page_primitives<'a>( |
| ... | @@ -2427,6 +2452,54 @@ mod tests { | ... | @@ -2427,6 +2452,54 @@ mod tests { |
| 2427 | ); | 2452 | ); |
| 2428 | } | 2453 | } |
| 2429 | | 2454 | |
| | 2455 | #[test] |
| | 2456 | fn a_covered_width_handle_stays_reachable_beside_the_outline_above_it() { |
| | 2457 | let mut engine = TextEngine::default(); |
| | 2458 | let pixel = 0.75; |
| | 2459 | let mut first = TextOutline::new( |
| | 2460 | &mut engine, |
| | 2461 | TextDocument::new(vec![Paragraph::new("text".into(), Format::default())]).unwrap(), |
| | 2462 | 100.0, |
| | 2463 | [36.0, 68.4], |
| | 2464 | ) |
| | 2465 | .unwrap(); |
| | 2466 | let editor = |
| | 2467 | CanvasEditor::from_text_outlines(vec![first.clone()], Default::default(), None) |
| | 2468 | .unwrap(); |
| | 2469 | first = editor.preview_resize(&mut engine, 226.5).unwrap(); |
| | 2470 | let (frame, body_top) = outline_chrome(&first, pixel); |
| | 2471 | // The casual night page: the next outline starts 7.5 pt right of the first one. |
| | 2472 | let second = TextOutline::new( |
| | 2473 | &mut engine, |
| | 2474 | TextDocument::new(vec![Paragraph::new("text".into(), Format::default())]).unwrap(), |
| | 2475 | 100.0, |
| | 2476 | [first.bounds().x1 as f32 + 7.5, 68.4], |
| | 2477 | ) |
| | 2478 | .unwrap(); |
| | 2479 | let (first_id, second_id) = (first.id, second.id); |
| | 2480 | let editor = |
| | 2481 | CanvasEditor::from_text_outlines(vec![first, second], Default::default(), None) |
| | 2482 | .unwrap(); |
| | 2483 | let header = (frame[1] + body_top) / 2.0; |
| | 2484 | let hit = |x| page_hit_test(&editor, None, [x, header], pixel); |
| | 2485 | assert!( |
| | 2486 | matches!(hit(frame[2] - 12.0 * pixel), Some(Hit::Resize { id, .. }) if id == first_id) |
| | 2487 | ); |
| | 2488 | assert!( |
| | 2489 | matches!(hit(frame[2] - 2.0 * pixel), Some(Hit::Handle { id, .. }) if id == second_id) |
| | 2490 | ); |
| | 2491 | let body = |x| page_hit_test(&editor, None, [x, body_top + 20.0], pixel); |
| | 2492 | assert!( |
| | 2493 | matches!(body(frame[2] - 4.0 * pixel), Some(Hit::Resize { id, .. }) if id == first_id) |
| | 2494 | ); |
| | 2495 | assert!( |
| | 2496 | matches!(body(frame[2] - 8.0 * pixel), Some(Hit::Text { id, .. }) if id == second_id) |
| | 2497 | ); |
| | 2498 | assert!( |
| | 2499 | matches!(body(frame[2] - 12.0 * pixel), Some(Hit::Text { id, .. }) if id == first_id) |
| | 2500 | ); |
| | 2501 | } |
| | 2502 | |
| 2430 | #[test] | 2503 | #[test] |
| 2431 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { | 2504 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { |
| 2432 | let selection = |start, end| { | 2505 | let selection = |start, end| { |