1use crate::core::alignment;
2use crate::core::layout;
3use crate::core::mouse;
4use crate::core::renderer;
5use crate::core::text::{Paragraph, Span};
6use crate::core::widget::text::{
7 self, Alignment, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping,
8};
9use crate::core::widget::tree::{self, Tree};
10use crate::core::{
11 self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point,
12 Rectangle, Shell, Size, Vector, Widget,
13};
14
15pub struct Rich<
17 'a,
18 Link,
19 Message,
20 Theme = crate::Theme,
21 Renderer = crate::Renderer,
22> where
23 Link: Clone + 'static,
24 Theme: Catalog,
25 Renderer: core::text::Renderer,
26{
27 spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
28 size: Option<Pixels>,
29 line_height: LineHeight,
30 width: Length,
31 height: Length,
32 font: Option<Renderer::Font>,
33 align_x: Alignment,
34 align_y: alignment::Vertical,
35 wrapping: Wrapping,
36 class: Theme::Class<'a>,
37 hovered_link: Option<usize>,
38 on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
39}
40
41impl<'a, Link, Message, Theme, Renderer>
42 Rich<'a, Link, Message, Theme, Renderer>
43where
44 Link: Clone + 'static,
45 Theme: Catalog,
46 Renderer: core::text::Renderer,
47 Renderer::Font: 'a,
48{
49 pub fn new() -> Self {
51 Self {
52 spans: Box::new([]),
53 size: None,
54 line_height: LineHeight::default(),
55 width: Length::Shrink,
56 height: Length::Shrink,
57 font: None,
58 align_x: Alignment::Default,
59 align_y: alignment::Vertical::Top,
60 wrapping: Wrapping::default(),
61 class: Theme::default(),
62 hovered_link: None,
63 on_link_click: None,
64 }
65 }
66
67 pub fn with_spans(
69 spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
70 ) -> Self {
71 Self {
72 spans: Box::new(spans),
73 ..Self::new()
74 }
75 }
76
77 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
79 self.size = Some(size.into());
80 self
81 }
82
83 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
85 self.line_height = line_height.into();
86 self
87 }
88
89 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
91 self.font = Some(font.into());
92 self
93 }
94
95 pub fn width(mut self, width: impl Into<Length>) -> Self {
97 self.width = width.into();
98 self
99 }
100
101 pub fn height(mut self, height: impl Into<Length>) -> Self {
103 self.height = height.into();
104 self
105 }
106
107 pub fn center(self) -> Self {
109 self.align_x(alignment::Horizontal::Center)
110 .align_y(alignment::Vertical::Center)
111 }
112
113 pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
115 self.align_x = alignment.into();
116 self
117 }
118
119 pub fn align_y(
121 mut self,
122 alignment: impl Into<alignment::Vertical>,
123 ) -> Self {
124 self.align_y = alignment.into();
125 self
126 }
127
128 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
130 self.wrapping = wrapping;
131 self
132 }
133
134 pub fn on_link_click(
137 mut self,
138 on_link_clicked: impl Fn(Link) -> Message + 'a,
139 ) -> Self {
140 self.on_link_click = Some(Box::new(on_link_clicked));
141 self
142 }
143
144 #[must_use]
146 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
147 where
148 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
149 {
150 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
151 self
152 }
153
154 pub fn color(self, color: impl Into<Color>) -> Self
156 where
157 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
158 {
159 self.color_maybe(Some(color))
160 }
161
162 pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
164 where
165 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
166 {
167 let color = color.map(Into::into);
168
169 self.style(move |_theme| Style { color })
170 }
171
172 #[cfg(feature = "advanced")]
174 #[must_use]
175 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
176 self.class = class.into();
177 self
178 }
179}
180
181impl<'a, Link, Message, Theme, Renderer> Default
182 for Rich<'a, Link, Message, Theme, Renderer>
183where
184 Link: Clone + 'a,
185 Theme: Catalog,
186 Renderer: core::text::Renderer,
187 Renderer::Font: 'a,
188{
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194struct State<Link, P: Paragraph> {
195 spans: Vec<Span<'static, Link, P::Font>>,
196 span_pressed: Option<usize>,
197 paragraph: P,
198}
199
200impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
201 for Rich<'_, Link, Message, Theme, Renderer>
202where
203 Link: Clone + 'static,
204 Theme: Catalog,
205 Renderer: core::text::Renderer,
206{
207 fn tag(&self) -> tree::Tag {
208 tree::Tag::of::<State<Link, Renderer::Paragraph>>()
209 }
210
211 fn state(&self) -> tree::State {
212 tree::State::new(State::<Link, _> {
213 spans: Vec::new(),
214 span_pressed: None,
215 paragraph: Renderer::Paragraph::default(),
216 })
217 }
218
219 fn size(&self) -> Size<Length> {
220 Size {
221 width: self.width,
222 height: self.height,
223 }
224 }
225
226 fn layout(
227 &mut self,
228 tree: &mut Tree,
229 renderer: &Renderer,
230 limits: &layout::Limits,
231 ) -> layout::Node {
232 layout(
233 tree.state
234 .downcast_mut::<State<Link, Renderer::Paragraph>>(),
235 renderer,
236 limits,
237 self.width,
238 self.height,
239 self.spans.as_ref().as_ref(),
240 self.line_height,
241 self.size,
242 self.font,
243 self.align_x,
244 self.align_y,
245 self.wrapping,
246 )
247 }
248
249 fn draw(
250 &self,
251 tree: &Tree,
252 renderer: &mut Renderer,
253 theme: &Theme,
254 defaults: &renderer::Style,
255 layout: Layout<'_>,
256 _cursor: mouse::Cursor,
257 viewport: &Rectangle,
258 ) {
259 if !layout.bounds().intersects(viewport) {
260 return;
261 }
262
263 let state = tree
264 .state
265 .downcast_ref::<State<Link, Renderer::Paragraph>>();
266
267 let style = theme.style(&self.class);
268
269 for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
270 let is_hovered_link = self.on_link_click.is_some()
271 && Some(index) == self.hovered_link;
272
273 if span.highlight.is_some()
274 || span.underline
275 || span.strikethrough
276 || is_hovered_link
277 {
278 let translation = layout.position() - Point::ORIGIN;
279 let regions = state.paragraph.span_bounds(index);
280
281 if let Some(highlight) = span.highlight {
282 for bounds in ®ions {
283 let bounds = Rectangle::new(
284 bounds.position()
285 - Vector::new(
286 span.padding.left,
287 span.padding.top,
288 ),
289 bounds.size()
290 + Size::new(
291 span.padding.horizontal(),
292 span.padding.vertical(),
293 ),
294 );
295
296 renderer.fill_quad(
297 renderer::Quad {
298 bounds: bounds + translation,
299 border: highlight.border,
300 ..Default::default()
301 },
302 highlight.background,
303 );
304 }
305 }
306
307 if span.underline || span.strikethrough || is_hovered_link {
308 let size = span
309 .size
310 .or(self.size)
311 .unwrap_or(renderer.default_size());
312
313 let line_height = span
314 .line_height
315 .unwrap_or(self.line_height)
316 .to_absolute(size);
317
318 let color = span
319 .color
320 .or(style.color)
321 .unwrap_or(defaults.text_color);
322
323 let baseline = translation
324 + Vector::new(
325 0.0,
326 size.0 + (line_height.0 - size.0) / 2.0,
327 );
328
329 if span.underline || is_hovered_link {
330 for bounds in ®ions {
331 renderer.fill_quad(
332 renderer::Quad {
333 bounds: Rectangle::new(
334 bounds.position() + baseline
335 - Vector::new(0.0, size.0 * 0.08),
336 Size::new(bounds.width, 1.0),
337 ),
338 ..Default::default()
339 },
340 color,
341 );
342 }
343 }
344
345 if span.strikethrough {
346 for bounds in ®ions {
347 renderer.fill_quad(
348 renderer::Quad {
349 bounds: Rectangle::new(
350 bounds.position() + baseline
351 - Vector::new(0.0, size.0 / 2.0),
352 Size::new(bounds.width, 1.0),
353 ),
354 ..Default::default()
355 },
356 color,
357 );
358 }
359 }
360 }
361 }
362 }
363
364 text::draw(
365 renderer,
366 defaults,
367 layout.bounds(),
368 &state.paragraph,
369 style,
370 viewport,
371 );
372 }
373
374 fn update(
375 &mut self,
376 tree: &mut Tree,
377 event: &Event,
378 layout: Layout<'_>,
379 cursor: mouse::Cursor,
380 _renderer: &Renderer,
381 _clipboard: &mut dyn Clipboard,
382 shell: &mut Shell<'_, Message>,
383 _viewport: &Rectangle,
384 ) {
385 let Some(on_link_clicked) = &self.on_link_click else {
386 return;
387 };
388
389 let was_hovered = self.hovered_link.is_some();
390
391 if let Some(position) = cursor.position_in(layout.bounds()) {
392 let state = tree
393 .state
394 .downcast_ref::<State<Link, Renderer::Paragraph>>();
395
396 self.hovered_link =
397 state.paragraph.hit_span(position).and_then(|span| {
398 if self.spans.as_ref().as_ref().get(span)?.link.is_some() {
399 Some(span)
400 } else {
401 None
402 }
403 });
404 } else {
405 self.hovered_link = None;
406 }
407
408 if was_hovered != self.hovered_link.is_some() {
409 shell.request_redraw();
410 }
411
412 match event {
413 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
414 let state = tree
415 .state
416 .downcast_mut::<State<Link, Renderer::Paragraph>>();
417
418 if self.hovered_link.is_some() {
419 state.span_pressed = self.hovered_link;
420 shell.capture_event();
421 }
422 }
423 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
424 let state = tree
425 .state
426 .downcast_mut::<State<Link, Renderer::Paragraph>>();
427
428 match state.span_pressed {
429 Some(span) if Some(span) == self.hovered_link => {
430 if let Some(link) = self
431 .spans
432 .as_ref()
433 .as_ref()
434 .get(span)
435 .and_then(|span| span.link.clone())
436 {
437 shell.publish(on_link_clicked(link));
438 }
439 }
440 _ => {}
441 }
442
443 state.span_pressed = None;
444 }
445 _ => {}
446 }
447 }
448
449 fn mouse_interaction(
450 &self,
451 _tree: &Tree,
452 _layout: Layout<'_>,
453 _cursor: mouse::Cursor,
454 _viewport: &Rectangle,
455 _renderer: &Renderer,
456 ) -> mouse::Interaction {
457 if self.hovered_link.is_some() {
458 mouse::Interaction::Pointer
459 } else {
460 mouse::Interaction::None
461 }
462 }
463}
464
465fn layout<Link, Renderer>(
466 state: &mut State<Link, Renderer::Paragraph>,
467 renderer: &Renderer,
468 limits: &layout::Limits,
469 width: Length,
470 height: Length,
471 spans: &[Span<'_, Link, Renderer::Font>],
472 line_height: LineHeight,
473 size: Option<Pixels>,
474 font: Option<Renderer::Font>,
475 align_x: Alignment,
476 align_y: alignment::Vertical,
477 wrapping: Wrapping,
478) -> layout::Node
479where
480 Link: Clone,
481 Renderer: core::text::Renderer,
482{
483 layout::sized(limits, width, height, |limits| {
484 let bounds = limits.max();
485
486 let size = size.unwrap_or_else(|| renderer.default_size());
487 let font = font.unwrap_or_else(|| renderer.default_font());
488
489 let text_with_spans = || core::Text {
490 content: spans,
491 bounds,
492 size,
493 line_height,
494 font,
495 align_x,
496 align_y,
497 shaping: Shaping::Advanced,
498 wrapping,
499 };
500
501 if state.spans != spans {
502 state.paragraph =
503 Renderer::Paragraph::with_spans(text_with_spans());
504 state.spans = spans.iter().cloned().map(Span::to_static).collect();
505 } else {
506 match state.paragraph.compare(core::Text {
507 content: (),
508 bounds,
509 size,
510 line_height,
511 font,
512 align_x,
513 align_y,
514 shaping: Shaping::Advanced,
515 wrapping,
516 }) {
517 core::text::Difference::None => {}
518 core::text::Difference::Bounds => {
519 state.paragraph.resize(bounds);
520 }
521 core::text::Difference::Shape => {
522 state.paragraph =
523 Renderer::Paragraph::with_spans(text_with_spans());
524 }
525 }
526 }
527
528 state.paragraph.min_bounds()
529 })
530}
531
532impl<'a, Link, Message, Theme, Renderer>
533 FromIterator<Span<'a, Link, Renderer::Font>>
534 for Rich<'a, Link, Message, Theme, Renderer>
535where
536 Link: Clone + 'a,
537 Theme: Catalog,
538 Renderer: core::text::Renderer,
539 Renderer::Font: 'a,
540{
541 fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>(
542 spans: T,
543 ) -> Self {
544 Self::with_spans(spans.into_iter().collect::<Vec<_>>())
545 }
546}
547
548impl<'a, Link, Message, Theme, Renderer>
549 From<Rich<'a, Link, Message, Theme, Renderer>>
550 for Element<'a, Message, Theme, Renderer>
551where
552 Message: 'a,
553 Link: Clone + 'a,
554 Theme: Catalog + 'a,
555 Renderer: core::text::Renderer + 'a,
556{
557 fn from(
558 text: Rich<'a, Link, Message, Theme, Renderer>,
559 ) -> Element<'a, Message, Theme, Renderer> {
560 Element::new(text)
561 }
562}