cranpose_ui/widgets/
clickable_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, TextOverflow, TextStyle},
15 widgets::BasicText,
16};
17
18#[doc(hidden)]
19pub trait IntoSharedAnnotatedString {
20 fn into_shared(self) -> Rc<AnnotatedString>;
21}
22
23impl IntoSharedAnnotatedString for AnnotatedString {
24 fn into_shared(self) -> Rc<AnnotatedString> {
25 Rc::new(self)
26 }
27}
28
29impl IntoSharedAnnotatedString for Rc<AnnotatedString> {
30 fn into_shared(self) -> Rc<AnnotatedString> {
31 self
32 }
33}
34
35pub fn ClickableText<T>(
65 text: T,
66 modifier: Modifier,
67 style: TextStyle,
68 on_click: impl Fn(usize) + 'static,
69) -> NodeId
70where
71 T: IntoSharedAnnotatedString,
72{
73 let text = text.into_shared();
74 let text_for_click = text.clone();
75 let style_for_click = style.clone();
76 let on_click: Rc<dyn Fn(usize)> = Rc::new(on_click);
77
78 let clickable_modifier = modifier.clickable(move |point| {
79 let offset = crate::text::get_offset_for_position(
80 &text_for_click,
81 &style_for_click,
82 point.x,
83 point.y,
84 );
85 on_click(offset);
86 });
87
88 BasicText(
89 text,
90 clickable_modifier,
91 style,
92 TextOverflow::Clip,
93 true,
94 usize::MAX,
95 1,
96 )
97}
98
99#[cfg(test)]
100mod tests {
101 use cranpose_core::{Composition, MemoryApplier, location_key};
102
103 use super::*;
104
105 #[test]
106 fn clickable_text_composes_without_panic() {
107 let _app_context = crate::render_state::app_context_test_scope();
108 let mut comp = Composition::new(MemoryApplier::new());
109 comp.render(location_key(file!(), line!(), column!()), || {
110 ClickableText(
111 AnnotatedString::from("Hello"),
112 Modifier::empty(),
113 TextStyle::default(),
114 |_offset| {},
115 );
116 })
117 .expect("composition succeeds");
118 }
119}