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
15#[allow(missing_debug_implementations)]
17pub struct Rich<
18 'a,
19 Link,
20 Message,
21 Theme = crate::Theme,
22 Renderer = crate::Renderer,
23> where
24 Link: Clone + 'static,
25 Theme: Catalog,
26 Renderer: core::text::Renderer,
27{
28 spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>,
29 size: Option<Pixels>,
30 line_height: LineHeight,
31 width: Length,
32 height: Length,
33 font: Option<Renderer::Font>,
34 align_x: Alignment,
35 align_y: alignment::Vertical,
36 wrapping: Wrapping,
37 class: Theme::Class<'a>,
38 hovered_link: Option<usize>,
39 on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>,
40}
41
42impl<'a, Link, Message, Theme, Renderer>
43 Rich<'a, Link, Message, Theme, Renderer>
44where
45 Link: Clone + 'static,
46 Theme: Catalog,
47 Renderer: core::text::Renderer,
48 Renderer::Font: 'a,
49{
50 pub fn new() -> Self {
52 Self {
53 spans: Box::new([]),
54 size: None,
55 line_height: LineHeight::default(),
56 width: Length::Shrink,
57 height: Length::Shrink,
58 font: None,
59 align_x: Alignment::Default,
60 align_y: alignment::Vertical::Top,
61 wrapping: Wrapping::default(),
62 class: Theme::default(),
63 hovered_link: None,
64 on_link_click: None,
65 }
66 }
67
68 pub fn with_spans(
70 spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a,
71 ) -> Self {
72 Self {
73 spans: Box::new(spans),
74 ..Self::new()
75 }
76 }
77
78 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
80 self.size = Some(size.into());
81 self
82 }
83
84 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
86 self.line_height = line_height.into();
87 self
88 }
89
90 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
92 self.font = Some(font.into());
93 self
94 }
95
96 pub fn width(mut self, width: impl Into<Length>) -> Self {
98 self.width = width.into();
99 self
100 }
101
102 pub fn height(mut self, height: impl Into<Length>) -> Self {
104 self.height = height.into();
105 self
106 }
107
108 pub fn center(self) -> Self {
110 self.align_x(alignment::Horizontal::Center)
111 .align_y(alignment::Vertical::Center)
112 }
113
114 pub fn align_x(mut self, alignment: impl Into<Alignment>) -> Self {
116 self.align_x = alignment.into();
117 self
118 }
119
120 pub fn align_y(
122 mut self,
123 alignment: impl Into<alignment::Vertical>,
124 ) -> Self {
125 self.align_y = alignment.into();
126 self
127 }
128
129 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
131 self.wrapping = wrapping;
132 self
133 }
134
135 pub fn on_link_click(
138 mut self,
139 on_link_clicked: impl Fn(Link) -> Message + 'a,
140 ) -> Self {
141 self.on_link_click = Some(Box::new(on_link_clicked));
142 self
143 }
144
145 #[must_use]
147 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
148 where
149 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
150 {
151 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
152 self
153 }
154
155 pub fn color(self, color: impl Into<Color>) -> Self
157 where
158 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
159 {
160 self.color_maybe(Some(color))
161 }
162
163 pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self
165 where
166 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
167 {
168 let color = color.map(Into::into);
169
170 self.style(move |_theme| Style { color })
171 }
172
173 #[cfg(feature = "advanced")]
175 #[must_use]
176 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
177 self.class = class.into();
178 self
179 }
180}
181
182impl<'a, Link, Message, Theme, Renderer> Default
183 for Rich<'a, Link, Message, Theme, Renderer>
184where
185 Link: Clone + 'a,
186 Theme: Catalog,
187 Renderer: core::text::Renderer,
188 Renderer::Font: 'a,
189{
190 fn default() -> Self {
191 Self::new()
192 }
193}
194
195struct State<Link, P: Paragraph> {
196 spans: Vec<Span<'static, Link, P::Font>>,
197 span_pressed: Option<usize>,
198 paragraph: P,
199}
200
201impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
202 for Rich<'_, Link, Message, Theme, Renderer>
203where
204 Link: Clone + 'static,
205 Theme: Catalog,
206 Renderer: core::text::Renderer,
207{
208 fn tag(&self) -> tree::Tag {
209 tree::Tag::of::<State<Link, Renderer::Paragraph>>()
210 }
211
212 fn state(&self) -> tree::State {
213 tree::State::new(State::<Link, _> {
214 spans: Vec::new(),
215 span_pressed: None,
216 paragraph: Renderer::Paragraph::default(),
217 })
218 }
219
220 fn size(&self) -> Size<Length> {
221 Size {
222 width: self.width,
223 height: self.height,
224 }
225 }
226
227 fn layout(
228 &self,
229 tree: &mut Tree,
230 renderer: &Renderer,
231 limits: &layout::Limits,
232 ) -> layout::Node {
233 layout(
234 tree.state
235 .downcast_mut::<State<Link, Renderer::Paragraph>>(),
236 renderer,
237 limits,
238 self.width,
239 self.height,
240 self.spans.as_ref().as_ref(),
241 self.line_height,
242 self.size,
243 self.font,
244 self.align_x,
245 self.align_y,
246 self.wrapping,
247 )
248 }
249
250 fn draw(
251 &self,
252 tree: &Tree,
253 renderer: &mut Renderer,
254 theme: &Theme,
255 defaults: &renderer::Style,
256 layout: Layout<'_>,
257 _cursor: mouse::Cursor,
258 viewport: &Rectangle,
259 ) {
260 if !layout.bounds().intersects(viewport) {
261 return;
262 }
263
264 let state = tree
265 .state
266 .downcast_ref::<State<Link, Renderer::Paragraph>>();
267
268 let style = theme.style(&self.class);
269
270 for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() {
271 let is_hovered_link = self.on_link_click.is_some()
272 && Some(index) == self.hovered_link;
273
274 if span.highlight.is_some()
275 || span.underline
276 || span.strikethrough
277 || is_hovered_link
278 {
279 let translation = layout.position() - Point::ORIGIN;
280 let regions = state.paragraph.span_bounds(index);
281
282 if let Some(highlight) = span.highlight {
283 for bounds in ®ions {
284 let bounds = Rectangle::new(
285 bounds.position()
286 - Vector::new(
287 span.padding.left,
288 span.padding.top,
289 ),
290 bounds.size()
291 + Size::new(
292 span.padding.horizontal(),
293 span.padding.vertical(),
294 ),
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,
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}