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}