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