Skip to main content

cranpose_ui/widgets/
linked_text.rs

1//! LinkedText widget — renders AnnotatedString with link annotations auto-handled.
2//!
3//! Mirrors the behaviour of Jetpack Compose `BasicText` / `Text` when the
4//! annotated string contains `LinkAnnotation.Url` or `LinkAnnotation.Clickable`.
5
6#![allow(non_snake_case)]
7
8use std::rc::Rc;
9
10use cranpose_core::NodeId;
11
12use crate::{
13    modifier::Modifier,
14    text::{AnnotatedString, LinkAnnotation, TextStyle},
15    widgets::ClickableText,
16};
17
18/// Renders an [`AnnotatedString`] and automatically dispatches link clicks:
19///
20/// - [`LinkAnnotation::Url`] → calls `open_url(url)` (platform provides the URI handler).
21/// - [`LinkAnnotation::Clickable`] → calls the handler stored in the annotation.
22///
23/// # Example — opening a URL
24///
25/// ```rust,ignore
26/// let uri_handler = local_uri_handler().current();
27/// let text = AnnotatedString::builder()
28///     .append("Visit the ")
29///     .with_link(
30///         LinkAnnotation::Url("https://developer.android.com/".into()),
31///         |b| b.append("Android Developers"),
32///     )
33///     .append(" site.")
34///     .to_annotated_string();
35///
36/// LinkedText(
37///     text,
38///     Modifier::empty(),
39///     TextStyle::default(),
40///     move |url| { uri_handler.open_uri(url).ok(); },
41/// );
42/// ```
43///
44/// # Example — custom action (`LinkAnnotation::Clickable`)
45///
46/// ```rust,ignore
47/// let text = AnnotatedString::builder()
48///     .append("Click ")
49///     .with_link(
50///         LinkAnnotation::Clickable {
51///             tag: "action".into(),
52///             handler: Rc::new(move || println!("clicked!")),
53///         },
54///         |b| b.append("here"),
55///     )
56///     .to_annotated_string();
57///
58/// // open_url is never called for Clickable — pass a no-op.
59/// LinkedText(text, Modifier::empty(), TextStyle::default(), |_| {});
60/// ```
61///
62/// # JC parity
63///
64/// Equivalent to `Text(buildAnnotatedString { withLink(LinkAnnotation.Url(…)) { … } })`.
65/// The `open_url` parameter corresponds to the platform-provided `LocalUriHandler`.
66#[allow(clippy::needless_pass_by_value)]
67pub fn LinkedText(
68    text: AnnotatedString,
69    modifier: Modifier,
70    style: TextStyle,
71    open_url: impl Fn(&str) + 'static,
72) -> NodeId {
73    let text = Rc::new(text);
74    let text_for_links = text.clone();
75    let open_url: Rc<dyn Fn(&str)> = Rc::new(open_url);
76
77    ClickableText(text, modifier, style, move |offset| {
78        for ann in text_for_links
79            .link_annotations
80            .iter()
81            .filter(|a| a.range.start <= offset && offset < a.range.end)
82        {
83            match &ann.item {
84                LinkAnnotation::Url(url) => open_url(url),
85                LinkAnnotation::Clickable { handler, .. } => handler(),
86            }
87        }
88    })
89}