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