Skip to main content

cranpose_ui/widgets/
clickable_text.rs

1//! ClickableText widget for handling clicks on annotated text.
2//!
3//! Mirrors Jetpack Compose's `ClickableText` from:
4//! `compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/ClickableText.kt`
5
6#![allow(non_snake_case)]
7
8use crate::modifier::Modifier;
9use crate::text::{AnnotatedString, TextOverflow, TextStyle};
10use crate::widgets::BasicText;
11use cranpose_core::NodeId;
12use std::rc::Rc;
13
14/// Displays an [`AnnotatedString`] and calls `on_click` with the **byte offset** of the character
15/// under the pointer at the time of the click.
16///
17/// Callers typically use the offset to query string annotations:
18///
19/// ```rust,ignore
20/// ClickableText(
21///     text.clone(),
22///     Modifier::empty(),
23///     TextStyle::default(),
24///     |offset| {
25///         for ann in text.get_string_annotations("URL", offset, offset + 1) {
26///             uri_handler.open_uri(&ann.item.annotation).ok();
27///         }
28///     },
29/// );
30/// ```
31///
32/// # JC parity
33///
34/// ```kotlin
35/// @Composable
36/// fun ClickableText(
37///     text: AnnotatedString,
38///     modifier: Modifier = Modifier,
39///     style: TextStyle = TextStyle.Default,
40///     onClick: (Int) -> Unit,
41/// )
42/// ```
43#[allow(clippy::needless_pass_by_value)]
44pub fn ClickableText(
45    text: AnnotatedString,
46    modifier: Modifier,
47    style: TextStyle,
48    on_click: impl Fn(usize) + 'static,
49) -> NodeId {
50    let text_for_click = text.clone();
51    let style_for_click = style.clone();
52    let on_click: Rc<dyn Fn(usize)> = Rc::new(on_click);
53
54    let clickable_modifier = modifier.clickable(move |point| {
55        let offset = crate::text::get_offset_for_position(
56            &text_for_click,
57            &style_for_click,
58            point.x,
59            point.y,
60        );
61        on_click(offset);
62    });
63
64    BasicText(
65        text,
66        clickable_modifier,
67        style,
68        TextOverflow::Clip,
69        true,
70        usize::MAX,
71        1,
72    )
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use cranpose_core::{location_key, Composition, MemoryApplier};
79
80    #[test]
81    fn clickable_text_composes_without_panic() {
82        let mut comp = Composition::new(MemoryApplier::new());
83        comp.render(location_key(file!(), line!(), column!()), || {
84            ClickableText(
85                AnnotatedString::from("Hello"),
86                Modifier::empty(),
87                TextStyle::default(),
88                |_offset| {},
89            );
90        })
91        .expect("composition succeeds");
92    }
93}