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}