Skip to main content

teksilo_widgets/rich_text/
hit_test.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Hit-test helpers and context-target classification.
5//!
6//! Pointer positions arrive in widget-local screen space. text-typeset's
7//! `Typesetter::hit_test` already compensates for the current scroll
8//! offset and zoom internally, so the widget forwards the pointer
9//! coordinate unchanged. The `ContextTarget` enum is exposed publicly
10//! so applications can build external context menus without reaching
11//! into the widget's private state.
12
13use teksilo_canvas::Point;
14use teksilo_text::text_document::{MoveMode, TextDocument};
15use teksilo_text::{HitRegion, HitTestResult, RichTextEngine};
16
17/// What was under a click, for context-menu classification.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum ContextTarget {
20    /// Ordinary text, not inside a selection.
21    Plain,
22    /// A click inside the current selection (for "Copy/Cut" menus).
23    InSelection,
24    /// A hyperlink.
25    Link { href: String },
26    /// An inline image.
27    Image { name: String },
28    /// A table cell. Row and column are zero-based.
29    TableCell {
30        table_id: usize,
31        row: usize,
32        col: usize,
33    },
34}
35
36/// Convenience wrapper over [`RichTextEngine::hit_test`]. The
37/// engine's hit-test accepts widget-local pointer coordinates
38/// directly — scroll offset and zoom are applied internally.
39pub fn hit_test_at(
40    engine: &RichTextEngine,
41    screen: Point,
42    _scroll_x: f32,
43    _scroll_y: f32,
44) -> Option<HitTestResult> {
45    engine.hit_test(screen.x, screen.y)
46}
47
48/// Classify what the user pointed at. Used by both the widget's own
49/// single-click dispatch and the application-facing
50/// `context_target_at()` helper. The `selection` argument is the current
51/// `(anchor, position)` of the widget's cursor in character offsets; if
52/// the hit position falls within that range we report `InSelection` so
53/// the context menu can show "Copy/Cut" without first collapsing the
54/// selection. `document` is used to resolve the table cell row/column
55/// for `TableCell` results — text-typeset's `HitTestResult::table_id`
56/// gives us the table, but only the document model knows the cell's
57/// row/column coordinates.
58pub fn classify(
59    hit: &HitTestResult,
60    selection: Option<(usize, usize)>,
61    document: &TextDocument,
62) -> ContextTarget {
63    // In-selection check first: a click inside the current selection
64    // reports `InSelection` regardless of whether the hit landed on a
65    // link, image, or plain text — the app's context menu typically
66    // wants "Cut/Copy/Paste" in that case rather than link-specific
67    // actions.
68    if let Some((anchor, caret)) = selection {
69        let (lo, hi) = (anchor.min(caret), anchor.max(caret));
70        if lo != hi && hit.position >= lo && hit.position <= hi {
71            return ContextTarget::InSelection;
72        }
73    }
74
75    match &hit.region {
76        HitRegion::Link { href } => ContextTarget::Link { href: href.clone() },
77        HitRegion::Image { name } => ContextTarget::Image { name: name.clone() },
78        _ => {
79            if let Some(table_id) = hit.table_id {
80                // Probe the document model for the cell's row/column.
81                // A fresh cursor at `hit.position` asks text-document
82                // to resolve the containing cell via
83                // `current_table_cell()`, which already understands
84                // row/column semantics.
85                let probe = document.cursor();
86                probe.set_position(hit.position, MoveMode::MoveAnchor);
87                if let Some(cell_ref) = probe.current_table_cell() {
88                    return ContextTarget::TableCell {
89                        table_id,
90                        row: cell_ref.row,
91                        col: cell_ref.column,
92                    };
93                }
94                return ContextTarget::TableCell {
95                    table_id,
96                    row: 0,
97                    col: 0,
98                };
99            }
100            ContextTarget::Plain
101        }
102    }
103}