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#![expect(non_snake_case)]
7
8use std::rc::Rc;
9
10use cranpose_core::NodeId;
11
12use crate::{
13    modifier::Modifier,
14    text::{AnnotatedString, TextOverflow, TextStyle},
15    widgets::BasicText,
16};
17
18#[doc(hidden)]
19pub trait IntoSharedAnnotatedString {
20    fn into_shared(self) -> Rc<AnnotatedString>;
21}
22
23impl IntoSharedAnnotatedString for AnnotatedString {
24    fn into_shared(self) -> Rc<AnnotatedString> {
25        Rc::new(self)
26    }
27}
28
29impl IntoSharedAnnotatedString for Rc<AnnotatedString> {
30    fn into_shared(self) -> Rc<AnnotatedString> {
31        self
32    }
33}
34
35/// Displays annotated text and calls `on_click` with the byte offset under the pointer.
36/// Use [`LinkedText`](crate::LinkedText) for URL and custom-action annotations.
37///
38/// # Example
39///
40/// ```rust
41/// use cranpose_ui::{text::AnnotatedString, *};
42///
43/// #[composable]
44/// fn OffsetLabel() {
45///     ClickableText(
46///         AnnotatedString::from("Click a character"),
47///         Modifier::empty(),
48///         TextStyle::default(),
49///         |offset| println!("Byte offset: {offset}"),
50///     );
51/// }
52/// ```
53pub fn ClickableText<T>(
54    text: T,
55    modifier: Modifier,
56    style: TextStyle,
57    on_click: impl Fn(usize) + 'static,
58) -> NodeId
59where
60    T: IntoSharedAnnotatedString,
61{
62    let text = text.into_shared();
63    let text_for_click = text.clone();
64    let style_for_click = style.clone();
65    let on_click: Rc<dyn Fn(usize)> = Rc::new(on_click);
66
67    let clickable_modifier = modifier.clickable(move |point| {
68        let offset = crate::text::get_offset_for_position(
69            &text_for_click,
70            &style_for_click,
71            point.x,
72            point.y,
73        );
74        on_click(offset);
75    });
76
77    BasicText(
78        text,
79        clickable_modifier,
80        style,
81        TextOverflow::Clip,
82        true,
83        usize::MAX,
84        1,
85    )
86}
87
88#[cfg(test)]
89#[path = "tests/clickable_text_tests.rs"]
90mod tests;