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