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