#![allow(non_snake_case)]
use std::rc::Rc;
use cranpose_core::NodeId;
use crate::{
modifier::Modifier,
text::{AnnotatedString, LinkAnnotation, TextStyle},
widgets::ClickableText,
};
#[allow(clippy::needless_pass_by_value)]
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)]
mod tests {
use std::cell::RefCell;
use super::*;
fn text_with_links() -> AnnotatedString {
AnnotatedString::builder()
.append("Read the ")
.with_link(
LinkAnnotation::Url("https://example.test/docs".into()),
|builder| builder.append("docs"),
)
.append(" or ")
.with_link(
LinkAnnotation::Clickable {
tag: "help".into(),
handler: Rc::new(|| {}),
},
|builder| builder.append("ask"),
)
.to_annotated_string()
}
#[test]
fn a_reader_opens_each_link_from_the_actions_menu() {
let opened = Rc::new(RefCell::new(Vec::new()));
let open_url: Rc<dyn Fn(&str)> = {
let opened = Rc::clone(&opened);
Rc::new(move |url: &str| opened.borrow_mut().push(url.to_owned()))
};
let mut config = cranpose_foundation::SemanticsConfiguration::default();
link_actions(Rc::new(text_with_links()), open_url)(&mut config);
let labels: Vec<_> = config
.custom_actions
.iter()
.map(|action| action.label.as_str())
.collect();
config.custom_actions[0].invoke();
assert_eq!(labels, ["Open docs", "Open ask"]);
assert_eq!(
*opened.borrow(),
vec!["https://example.test/docs".to_owned()]
);
}
#[test]
fn a_link_with_no_shown_text_is_named_by_its_target() {
let text = AnnotatedString::builder()
.with_link(
LinkAnnotation::Url("https://example.test/".into()),
|builder| builder,
)
.to_annotated_string();
let mut config = cranpose_foundation::SemanticsConfiguration::default();
link_actions(Rc::new(text), Rc::new(|_: &str| {}))(&mut config);
assert_eq!(config.custom_actions[0].label, "Open https://example.test/");
}
}