| ... | ... | @@ -1596,8 +1596,17 @@ fn page_hit_test( |
| 1596 | 1596 | point: [f32; 2], |
| 1597 | 1597 | pixel: f32, |
| 1598 | 1598 | ) -> Option<Hit> { |
| 1599 | | let hit = |outline: &TextOutline, offset: [f32; 2], below: bool| { |
| 1600 | | if below { |
| 1599 | /// Grips and text resolve front to back before any outline's padding, as a native |
| 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 | 1610 | let outline = editor |
| 1602 | 1611 | .outlines() |
| 1603 | 1612 | .iter() |
| ... | ... | @@ -1614,20 +1623,34 @@ fn page_hit_test( |
| 1614 | 1623 | let (bounds, body_top) = outline_chrome(outline, pixel); |
| 1615 | 1624 | let local = [point[0] - offset[0], point[1] - offset[1]]; |
| 1616 | 1625 | let [x, y] = local; |
| 1617 | | if x >= bounds[0] && x <= bounds[2] && y >= bounds[1] && y < body_top { |
| 1618 | | if x >= bounds[2] - 9.0 * pixel { |
| 1626 | if layer == Layer::Grips { |
| 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 | 1634 | return Some(Hit::Resize { |
| 1620 | 1635 | id: outline.id, |
| 1621 | 1636 | grab: point[0] - outline.bounds().x1 as f32, |
| 1622 | 1637 | }); |
| 1623 | 1638 | } |
| 1624 | | return Some(Hit::Handle { |
| 1625 | | id: outline.id, |
| 1626 | | grab: [ |
| 1627 | | point[0] - outline.origin()[0], |
| 1628 | | point[1] - outline.origin()[1], |
| 1629 | | ], |
| 1630 | | }); |
| 1639 | if x >= bounds[0] && x <= bounds[2] && y >= bounds[1] && y < body_top { |
| 1640 | return Some(Hit::Handle { |
| 1641 | id: outline.id, |
| 1642 | grab: [ |
| 1643 | point[0] - outline.origin()[0], |
| 1644 | point[1] - outline.origin()[1], |
| 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 | 1655 | if (x >= bounds[0] && x <= bounds[2] && y >= body_top && y <= bounds[3]) |
| 1633 | 1656 | || outline.layouts().any(|(_, paragraph)| { |
| ... | ... | @@ -1650,12 +1673,12 @@ fn page_hit_test( |
| 1650 | 1673 | } |
| 1651 | 1674 | None |
| 1652 | 1675 | }; |
| 1653 | | let pass = |below| { |
| 1676 | let pass = |layer| { |
| 1654 | 1677 | if let Some(hit) = editor |
| 1655 | 1678 | .visible_outlines() |
| 1656 | 1679 | .rev() |
| 1657 | 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 | 1683 | return Some(hit); |
| 1661 | 1684 | } |
| ... | ... | @@ -1667,7 +1690,7 @@ fn page_hit_test( |
| 1667 | 1690 | editor |
| 1668 | 1691 | .visible_outlines() |
| 1669 | 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 | 1696 | canvas::gpu::page::SceneHit::Outline(hit) => Some(hit), |
| ... | ... | @@ -1675,7 +1698,9 @@ fn page_hit_test( |
| 1675 | 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 | 1706 | fn page_primitives<'a>( |
| ... | ... | @@ -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 | 2503 | #[test] |
| 2431 | 2504 | fn grouped_drag_keeps_the_initial_word_when_reversing_direction() { |
| 2432 | 2505 | let selection = |start, end| { |