1use std::{any::Any, cell::RefCell, rc::Rc};
2
3use floem_reactive::Effect;
4use floem_renderer::text::{Attrs, AttrsList, AttrsOwned};
5use peniko::{Color, color::palette};
6use smallvec::{SmallVec, smallvec};
7use taffy::tree::NodeId;
8
9use crate::{IntoView, context::UpdateCx, view::LayoutNodeCx, view::View, view::ViewId};
10
11use crate::text::TextLayoutState;
12
13pub struct RichText {
14 id: ViewId,
15 layout_data: Rc<RefCell<TextLayoutState>>,
17 text_node: Option<NodeId>,
18 layout_node: Option<NodeId>,
19}
20
21pub fn rich_text(
22 text: String,
23 attrs_list: AttrsList,
24 text_fn: impl Fn() -> (String, AttrsList) + 'static,
25) -> RichText {
26 let id = ViewId::new();
27 Effect::new(move |_| {
28 let (new_text, new_attrs) = text_fn();
29 id.update_state((new_text, new_attrs));
30 });
31
32 let layout_data = Rc::new(RefCell::new(TextLayoutState::new(Some(id))));
33
34 {
36 let mut data = layout_data.borrow_mut();
37 data.set_text(&text, attrs_list, None);
38 data.set_text_overflow(crate::style::TextOverflow::Wrap {
39 overflow_wrap: crate::text::OverflowWrap::Normal,
40 word_break: crate::text::WordBreakStrength::Normal,
41 });
42 }
43
44 let mut rich_text = RichText {
45 id,
46 layout_data,
47 text_node: None,
48 layout_node: None,
49 };
50
51 rich_text.set_taffy_layout();
52 rich_text
53}
54
55impl RichText {
56 fn mark_text_measure_dirty(&self) {
57 if let Some(text_node) = self.text_node {
58 let _ = self.id.taffy().borrow_mut().mark_dirty(text_node);
59 }
60 let _ = self.id.mark_view_layout_dirty();
61 }
62
63 fn set_taffy_layout(&mut self) {
64 let taffy_node = self.id.taffy_node();
65 let taffy = self.id.taffy();
66 let mut taffy = taffy.borrow_mut();
67 let text_node = taffy
68 .new_leaf(taffy::Style {
69 ..taffy::Style::DEFAULT
70 })
71 .unwrap();
72
73 let layout_fn = TextLayoutState::create_taffy_layout_fn(self.layout_data.clone());
74 let finalize_fn = TextLayoutState::create_finalize_fn(self.layout_data.clone());
75 self.text_node = Some(text_node);
76 self.layout_node = Some(taffy_node);
77
78 taffy
79 .set_node_context(
80 text_node,
81 Some(LayoutNodeCx::Custom {
82 measure: layout_fn,
83 finalize: Some(finalize_fn),
84 }),
85 )
86 .unwrap();
87 taffy.set_children(taffy_node, &[text_node]).unwrap();
88 }
89}
90
91impl View for RichText {
92 fn id(&self) -> ViewId {
93 self.id
94 }
95
96 fn debug_name(&self) -> std::borrow::Cow<'static, str> {
97 self.layout_data
98 .borrow()
99 .get_text_layout()
100 .map(|_| {
101 let data = self.layout_data.borrow();
102 format!(
103 "RichText: {:?}",
104 crate::text::paragraph_ranges(data.text().unwrap_or_default())
105 .map(|r| data.text().unwrap_or_default()[r].to_string())
106 .collect::<Vec<_>>()
107 )
108 })
109 .unwrap_or_else(|| "RichText: <empty>".to_string())
110 .into()
111 }
112
113 fn update(&mut self, _cx: &mut UpdateCx, state: Box<dyn Any>) {
114 if let Ok(state) = state.downcast::<(String, AttrsList)>() {
115 let (text, attrs_list) = *state;
116
117 let mut data = self.layout_data.borrow_mut();
118 data.set_text(&text, attrs_list, None);
119 data.set_text_overflow(crate::style::TextOverflow::Wrap {
120 overflow_wrap: crate::text::OverflowWrap::Normal,
121 word_break: crate::text::WordBreakStrength::Normal,
122 });
123 drop(data);
124
125 self.mark_text_measure_dirty();
126 self.id.request_layout();
127 }
128 }
129
130 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
131 let (Some(parent_node), Some(text_node)) = (self.layout_node, self.text_node) else {
132 return;
133 };
134
135 let text_loc = self
136 .id
137 .get_content_rect_relative(text_node, parent_node)
138 .unwrap_or_default()
139 .origin();
140
141 self.layout_data
142 .borrow()
143 .with_effective_text_layout(|layout| {
144 layout.draw(cx, text_loc);
145 });
146 }
147}
148
149#[derive(Clone, Debug)]
150pub struct RichSpan<'a> {
151 text: &'a str,
152 attrs: Attrs<'a>,
153}
154#[allow(clippy::wrong_self_convention)]
155impl<'a> RichSpan<'a> {
156 fn to_owned(self) -> RichSpanOwned {
157 let len = self.text.len();
158 RichSpanOwned {
159 text: self.text.to_string(),
160 spans: smallvec::smallvec![(0..len, AttrsOwned::new(self.attrs))],
161 }
162 }
163 pub fn color(mut self, color: Color) -> Self {
164 self.attrs = self.attrs.color(color);
165 self
166 }
167
168 pub fn family(mut self, family: &'a [floem_renderer::text::FamilyOwned]) -> RichSpan<'a> {
169 self.attrs = self.attrs.family(family);
170 self
171 }
172
173 pub fn font_width(mut self, stretch: floem_renderer::text::FontWidth) -> RichSpan<'a> {
174 self.attrs = self.attrs.font_width(stretch);
175 self
176 }
177
178 pub fn text_style(mut self, style: floem_renderer::text::FontStyle) -> RichSpan<'a> {
179 self.attrs = self.attrs.font_style(style);
180 self
181 }
182
183 pub fn weight(mut self, weight: floem_renderer::text::FontWeight) -> RichSpan<'a> {
184 self.attrs = self.attrs.weight(weight);
185 self
186 }
187
188 pub fn line_height(
189 mut self,
190 line_height: floem_renderer::text::LineHeightValue,
191 ) -> RichSpan<'a> {
192 self.attrs = self.attrs.line_height(line_height);
193 self
194 }
195
196 pub fn font_size(mut self, font_size: f32) -> RichSpan<'a> {
197 self.attrs = self.attrs.font_size(font_size);
198 self
199 }
200
201 pub fn raw_weight(mut self, weight: u16) -> RichSpan<'a> {
202 self.attrs = self.attrs.raw_weight(weight);
203 self
204 }
205}
206#[derive(Clone, Debug)]
207pub struct RichSpanOwned {
208 text: String,
209 spans: SmallVec<[(std::ops::Range<usize>, AttrsOwned); 3]>,
210}
211impl IntoView for RichSpanOwned {
212 type V = RichText;
213 type Intermediate = RichText;
214
215 fn into_intermediate(self) -> Self::Intermediate {
216 let mut attrs_list = AttrsList::new(Attrs::new().color(palette::css::BLACK));
217 for span in self.spans.clone() {
218 attrs_list.add_span(span.0, span.1.as_attrs());
219 }
220
221 let text = self.text.clone();
222 let text_clone = self.text.clone();
223 let spans = self.spans.clone();
224
225 rich_text(text, attrs_list, move || {
226 let mut attrs_list = AttrsList::new(Attrs::new().color(palette::css::BLACK));
227 for span in spans.clone() {
228 attrs_list.add_span(span.0, span.1.as_attrs());
229 }
230 (text_clone.clone(), attrs_list)
231 })
232 }
233}
234impl<'a> IntoView for RichSpan<'a> {
235 type V = RichText;
236 type Intermediate = RichText;
237
238 fn into_intermediate(self) -> Self::Intermediate {
239 self.to_owned().into_intermediate()
240 }
241}
242impl<'a, S> std::ops::Add<S> for RichSpan<'a>
243where
244 RichSpan<'a>: From<S>,
245{
246 type Output = RichSpanOwned;
247
248 fn add(self, rhs: S) -> Self::Output {
249 let self_len = self.text.len();
250 let rhs: RichSpan = rhs.into();
251 let rhs_len = rhs.text.len();
252 RichSpanOwned {
253 text: self.text.to_string() + rhs.text,
254 spans: smallvec![
255 (0..self_len, AttrsOwned::new(self.attrs)),
256 (self_len..self_len + rhs_len, AttrsOwned::new(rhs.attrs)),
257 ],
258 }
259 }
260}
261impl<'a> std::ops::Add<&'a str> for RichSpan<'a> {
262 type Output = RichSpanOwned;
263
264 fn add(self, rhs: &'a str) -> Self::Output {
265 let self_len = self.text.len();
266 let rhs_len = rhs.len();
267 RichSpanOwned {
268 text: self.text.to_string() + rhs,
269 spans: smallvec![
270 (0..self_len, AttrsOwned::new(self.attrs)),
271 (
272 self_len..self_len + rhs_len,
273 AttrsOwned::new(Attrs::new().color(palette::css::BLACK))
274 ),
275 ],
276 }
277 }
278}
279impl std::ops::Add<String> for RichSpan<'_> {
280 type Output = RichSpanOwned;
281
282 fn add(self, rhs: String) -> Self::Output {
283 let self_len = self.text.len();
284 let rhs_len = rhs.len();
285 RichSpanOwned {
286 text: self.text.to_string() + &rhs,
287 spans: smallvec![
288 (0..self_len, AttrsOwned::new(self.attrs)),
289 (
290 self_len..self_len + rhs_len,
291 AttrsOwned::new(Attrs::new().color(palette::css::BLACK))
292 ),
293 ],
294 }
295 }
296}
297impl<'a, S> std::ops::Add<S> for RichSpanOwned
298where
299 RichSpan<'a>: From<S>,
300{
301 type Output = Self;
302
303 fn add(mut self, rhs: S) -> Self::Output {
304 let rhs: RichSpan = rhs.into();
305 let self_len = self.text.len();
306 let new_text = self.text + rhs.text;
307 self.spans
308 .push((self_len..new_text.len(), AttrsOwned::new(rhs.attrs)));
309 Self {
310 text: new_text,
311 spans: self.spans,
312 }
313 }
314}
315impl std::ops::Add<&str> for RichSpanOwned {
316 type Output = RichSpanOwned;
317
318 fn add(mut self, rhs: &str) -> Self::Output {
319 let self_len = self.text.len();
320 let new_text = self.text + rhs;
321 self.spans.push((
322 self_len..new_text.len(),
323 AttrsOwned::new(Attrs::new().color(palette::css::BLACK)),
324 ));
325 Self {
326 text: new_text,
327 spans: self.spans,
328 }
329 }
330}
331impl std::ops::Add<String> for RichSpanOwned {
332 type Output = RichSpanOwned;
333
334 fn add(mut self, rhs: String) -> Self::Output {
335 let self_len = self.text.len();
336 let new_text = self.text + &rhs;
337 self.spans.push((
338 self_len..new_text.len(),
339 AttrsOwned::new(Attrs::new().color(palette::css::BLACK)),
340 ));
341 Self {
342 text: new_text,
343 spans: self.spans,
344 }
345 }
346}
347impl std::ops::Add for RichSpanOwned {
348 type Output = Self;
349
350 fn add(mut self, rhs: Self) -> Self::Output {
351 let self_len = self.text.len();
352 self.spans.extend(
353 rhs.spans
354 .into_iter()
355 .map(|span| ((span.0.start + self_len)..(span.0.end + self_len), span.1)),
356 );
357 Self {
358 text: self.text + &rhs.text,
359 spans: self.spans,
360 }
361 }
362}
363
364pub trait RichTextExt<'a>
365where
366 Self: Sized,
367 RichSpan<'a>: From<Self>,
368{
369 fn color(self, color: Color) -> RichSpan<'a> {
370 let span: RichSpan = self.into();
371 span.color(color)
372 }
373 fn red(self) -> RichSpan<'a> {
374 self.color(palette::css::RED)
375 }
376 fn blue(self) -> RichSpan<'a> {
377 self.color(palette::css::BLUE)
378 }
379
380 fn green(self) -> RichSpan<'a> {
381 self.color(palette::css::GREEN)
382 }
383
384 fn yellow(self) -> RichSpan<'a> {
385 self.color(palette::css::YELLOW)
386 }
387
388 fn black(self) -> RichSpan<'a> {
389 self.color(palette::css::BLACK)
390 }
391
392 fn white(self) -> RichSpan<'a> {
393 self.color(palette::css::WHITE)
394 }
395
396 fn gray(self) -> RichSpan<'a> {
397 self.color(palette::css::GRAY)
398 }
399
400 fn cyan(self) -> RichSpan<'a> {
401 self.color(palette::css::CYAN)
402 }
403
404 fn magenta(self) -> RichSpan<'a> {
405 self.color(palette::css::MAGENTA)
406 }
407
408 fn orange(self) -> RichSpan<'a> {
409 self.color(palette::css::ORANGE)
410 }
411
412 fn purple(self) -> RichSpan<'a> {
413 self.color(palette::css::PURPLE)
414 }
415
416 fn pink(self) -> RichSpan<'a> {
417 self.color(palette::css::PINK)
418 }
419
420 fn family(self, family: &'a [crate::text::FamilyOwned]) -> RichSpan<'a> {
421 let span: RichSpan = self.into();
422 span.family(family)
423 }
424 fn stretch(self, stretch: crate::text::FontWidth) -> RichSpan<'a> {
425 let span: RichSpan = self.into();
426 span.font_width(stretch)
427 }
428 fn text_style(self, style: crate::text::FontStyle) -> RichSpan<'a> {
429 let span: RichSpan = self.into();
430 span.text_style(style)
431 }
432 fn italic(self) -> RichSpan<'a> {
433 self.text_style(crate::text::FontStyle::Italic)
434 }
435 fn oblique(self) -> RichSpan<'a> {
436 self.text_style(crate::text::FontStyle::Oblique(None))
437 }
438
439 fn weight(self, weight: crate::text::FontWeight) -> RichSpan<'a> {
440 let span: RichSpan = self.into();
441 span.weight(weight)
442 }
443 fn thin(self) -> RichSpan<'a> {
444 self.weight(crate::text::FontWeight::THIN)
445 }
446 fn extra_light(self) -> RichSpan<'a> {
447 self.weight(crate::text::FontWeight::EXTRA_LIGHT)
448 }
449 fn light(self) -> RichSpan<'a> {
450 self.weight(crate::text::FontWeight::LIGHT)
451 }
452 fn medium(self) -> RichSpan<'a> {
453 self.weight(crate::text::FontWeight::MEDIUM)
454 }
455 fn semibold(self) -> RichSpan<'a> {
456 self.weight(crate::text::FontWeight::SEMI_BOLD)
457 }
458 fn bold(self) -> RichSpan<'a> {
459 self.weight(crate::text::FontWeight::BOLD)
460 }
461 fn extra_bold(self) -> RichSpan<'a> {
462 self.weight(crate::text::FontWeight::EXTRA_BOLD)
463 }
464
465 fn raw_weight(self, weight: u16) -> RichSpan<'a> {
466 let span: RichSpan = self.into();
467 span.raw_weight(weight)
468 }
469 fn font_size(self, font_size: f32) -> RichSpan<'a> {
470 let span: RichSpan = self.into();
471 span.font_size(font_size)
472 }
473
474 fn line_height(self, line_height: crate::text::LineHeightValue) -> RichSpan<'a> {
475 let span: RichSpan = self.into();
476 span.line_height(line_height)
477 }
478}
479
480impl<'a, S> RichTextExt<'a> for S
481where
482 S: AsRef<str>,
483 RichSpan<'a>: From<S>,
484{
485}
486impl<'a, S: AsRef<str> + 'a> From<&'a S> for RichSpan<'a> {
487 fn from(value: &'a S) -> Self {
488 RichSpan {
489 text: value.as_ref(),
490 attrs: Attrs::new().color(palette::css::BLACK),
491 }
492 }
493}
494impl<'a> RichTextExt<'a> for RichSpan<'a> {}