#![expect(non_snake_case)]
use std::rc::Rc;
use cranpose_core::NodeId;
use crate::{
modifier::Modifier,
text::{AnnotatedString, LinkAnnotation, TextStyle},
widgets::ClickableText,
};
pub fn LinkedText(
text: AnnotatedString,
modifier: Modifier,
style: TextStyle,
open_url: impl Fn(&str) + 'static,
) -> NodeId {
let text = Rc::new(text);
let text_for_links = text.clone();
let open_url: Rc<dyn Fn(&str)> = Rc::new(open_url);
let modifier = modifier.semantics(link_actions(Rc::clone(&text), Rc::clone(&open_url)));
ClickableText(text, modifier, style, move |offset| {
for ann in text_for_links
.link_annotations
.iter()
.filter(|a| a.range.start <= offset && offset < a.range.end)
{
match &ann.item {
LinkAnnotation::Url(url) => open_url(url),
LinkAnnotation::Clickable { handler, .. } => handler(),
}
}
})
}
fn link_actions(
text: Rc<AnnotatedString>,
open_url: Rc<dyn Fn(&str)>,
) -> impl Fn(&mut cranpose_foundation::SemanticsConfiguration) {
move |config| {
for link in &text.link_annotations {
let shown = text.text.get(link.range.clone());
let (label, action) = link_action(shown, &link.item, &open_url);
config
.custom_actions
.push(cranpose_foundation::SemanticsCustomAction::new(
label,
move || action(),
));
}
}
}
fn link_action(
shown: Option<&str>,
link: &LinkAnnotation,
open_url: &Rc<dyn Fn(&str)>,
) -> (String, Rc<dyn Fn()>) {
let shown = shown.map(str::trim).filter(|shown| !shown.is_empty());
match link {
LinkAnnotation::Url(url) => {
let label = format!("Open {}", shown.unwrap_or(url));
let open_url = Rc::clone(open_url);
let url = url.clone();
(label, Rc::new(move || open_url(&url)))
}
LinkAnnotation::Clickable { tag, handler } => {
let label = format!("Open {}", shown.unwrap_or(tag));
(label, Rc::clone(handler))
}
}
}
#[cfg(test)]
#[path = "tests/linked_text_tests.rs"]
mod tests;