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