cranpose_ui/widgets/
linked_text.rs1#![expect(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
18pub fn LinkedText(
44 text: impl Into<Rc<AnnotatedString>>,
45 modifier: Modifier,
46 style: TextStyle,
47 open_url: impl Fn(&str) + 'static,
48) -> NodeId {
49 let text = text.into();
50 let text_for_links = text.clone();
51 let open_url: Rc<dyn Fn(&str)> = Rc::new(open_url);
52 let modifier = modifier.stable_semantics(link_actions(Rc::clone(&text), Rc::clone(&open_url)));
53
54 ClickableText(text, modifier, style, move |offset| {
55 for ann in text_for_links
56 .link_annotations
57 .iter()
58 .filter(|a| a.range.start <= offset && offset < a.range.end)
59 {
60 match &ann.item {
61 LinkAnnotation::Url(url) => open_url(url),
62 LinkAnnotation::Clickable { handler, .. } => handler(),
63 }
64 }
65 })
66}
67
68fn link_actions(
71 text: Rc<AnnotatedString>,
72 open_url: Rc<dyn Fn(&str)>,
73) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) {
74 move |config| {
75 for link in &text.link_annotations {
76 let shown = text.text.get(link.range.clone());
77 let (label, action) = link_action(shown, &link.item, &open_url);
78 config
79 .custom_actions
80 .push(cranpose_foundation::SemanticsCustomAction::new(
81 label,
82 move || action(),
83 ));
84 }
85 }
86}
87
88fn link_action(
89 shown: Option<&str>,
90 link: &LinkAnnotation,
91 open_url: &Rc<dyn Fn(&str)>,
92) -> (String, Rc<dyn Fn()>) {
93 let shown = shown.map(str::trim).filter(|shown| !shown.is_empty());
94 match link {
95 LinkAnnotation::Url(url) => {
96 let label = format!("Open {}", shown.unwrap_or(url));
97 let open_url = Rc::clone(open_url);
98 let url = url.clone();
99 (label, Rc::new(move || open_url(&url)))
100 }
101 LinkAnnotation::Clickable { tag, handler } => {
102 let label = format!("Open {}", shown.unwrap_or(tag));
103 (label, Rc::clone(handler))
104 }
105 }
106}
107
108#[cfg(test)]
109#[path = "tests/linked_text_tests.rs"]
110mod tests;